pax_global_header00006660000000000000000000000064136262341350014517gustar00rootroot0000000000000052 comment=f819839f404e95479e5640e6b2f07e89ed78566a javaproperties-0.6.0/000077500000000000000000000000001362623413500145605ustar00rootroot00000000000000javaproperties-0.6.0/.gitignore000066400000000000000000000001631362623413500165500ustar00rootroot00000000000000*.egg *.egg-info/ *.pyc .cache/ .coverage .eggs/ .pytest_cache/ .tox/ __pycache__/ build/ dist/ docs/_build/ venv/ javaproperties-0.6.0/.travis.yml000066400000000000000000000003541362623413500166730ustar00rootroot00000000000000language: python cache: pip dist: xenial python: - '2.7' - '3.4' - '3.5' - '3.6' - '3.7' - '3.8' - pypy - pypy3 install: - pip install codecov tox script: - tox -e py after_success: - codecov javaproperties-0.6.0/CHANGELOG.md000066400000000000000000000046321362623413500163760ustar00rootroot00000000000000v0.6.0 (2020-02-28) ------------------- - Include changelog in the Read the Docs site - Support Python 3.8 - When dumping a value that begins with more than one space, only escape the first space in order to better match Java's behavior - Gave `dump()`, `dumps()`, `escape()`, and `join_key_value()` an `ensure_ascii` parameter for optionally not escaping non-ASCII characters in output - Gave `dump()` and `dumps()` an `ensure_ascii_comments` parameter for controlling what characters in the `comments` parameter are escaped - Gave `to_comment()` an `ensure_ascii` parameter for controlling what characters are escaped - Added a custom encoding error handler `'javapropertiesreplace'` that encodes invalid characters as `\uXXXX` escape sequences v0.5.2 (2019-04-08) ------------------- - Added an example of each format to the format descriptions in the docs - Fix building in non-UTF-8 environments v0.5.1 (2018-10-25) ------------------- - **Bugfix**: `java_timestamp()` now properly handles naΓ―ve `datetime` objects with `fold=1` - Include installation instructions, examples, and GitHub links in the Read the Docs site v0.5.0 (2018-09-18) ------------------- - **Breaking**: Invalid `\uXXXX` escape sequences now cause an `InvalidUEscapeError` to be raised - `Properties` instances can now compare equal to `dict`s and other mapping types - Gave `Properties` a `copy` method - Drop support for Python 2.6 and 3.3 - Fixed a `DeprecationWarning` in Python 3.7 v0.4.0 (2017-04-22) ------------------- - Split off the command-line programs into a separate package, [`javaproperties-cli`](https://github.com/jwodder/javaproperties-cli) v0.3.0 (2017-04-13) ------------------- - Added the `PropertiesFile` class for preserving comments in files [#1] - The `ordereddict` package is now required under Python 2.6 v0.2.1 (2017-03-20) ------------------- - **Bugfix** to `javaproperties` command: Don't die horribly on missing non-ASCII keys - PyPy now supported v0.2.0 (2016-11-14) ------------------- - Added a `javaproperties` command for basic command-line manipulating of `.properties` files - Gave `json2properties` a `--separator` option - Gave `json2properties` and `properties2json` `--encoding` options - Exported the `java_timestamp()` function - `to_comment` now converts CR LF and CR line endings inside comments to LF - Some minor documentation improvements v0.1.0 (2016-10-02) ------------------- Initial release javaproperties-0.6.0/LICENSE000066400000000000000000000021071362623413500155650ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016-2020 John Thorvald Wodder II Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. javaproperties-0.6.0/MANIFEST.in000066400000000000000000000001651362623413500163200ustar00rootroot00000000000000include CHANGELOG.* CONTRIBUTORS.* LICENSE tox.ini graft docs prune docs/_build global-exclude *.py[cod] __pycache__ javaproperties-0.6.0/README.rst000066400000000000000000000110451362623413500162500ustar00rootroot00000000000000.. image:: http://www.repostatus.org/badges/latest/active.svg :target: http://www.repostatus.org/#active :alt: Project Status: Active - The project has reached a stable, usable state and is being actively developed. .. image:: https://travis-ci.org/jwodder/javaproperties.svg?branch=master :target: https://travis-ci.org/jwodder/javaproperties .. image:: https://codecov.io/gh/jwodder/javaproperties/branch/master/graph/badge.svg :target: https://codecov.io/gh/jwodder/javaproperties .. image:: https://img.shields.io/pypi/pyversions/javaproperties.svg :target: https://pypi.org/project/javaproperties .. image:: https://img.shields.io/github/license/jwodder/javaproperties.svg?maxAge=2592000 :target: https://opensource.org/licenses/MIT :alt: MIT License .. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg :target: https://saythanks.io/to/jwodder `GitHub `_ | `PyPI `_ | `Documentation `_ | `Issues `_ | `Changelog `_ ``javaproperties`` provides support for reading & writing |properties|_ (both the simple line-oriented format and XML) with a simple API based on the ``json`` module β€” though, for recovering Java addicts, it also includes a ``Properties`` class intended to match the behavior of |propclass|_ as much as is Pythonically possible. Previous versions of ``javaproperties`` included command-line programs for basic manipulation of ``.properties`` files. As of version 0.4.0, these programs have been split off into a separate package, |clipkg|_. Installation ============ Just use `pip `_ (You have pip, right?) to install ``javaproperties`` and its dependencies:: pip install javaproperties Examples ======== Dump some keys & values (output order not guaranteed):: >>> properties = {"key": "value", "host:port": "127.0.0.1:80", "snowman": "β˜ƒ", "goat": "🐐"} >>> print(javaproperties.dumps(properties)) #Mon Sep 26 14:57:44 EDT 2016 key=value goat=\ud83d\udc10 host\:port=127.0.0.1\:80 snowman=\u2603 Load some keys & values:: >>> javaproperties.loads(''' ... #Mon Sep 26 14:57:44 EDT 2016 ... key = value ... goat: \\ud83d\\udc10 ... host\\:port=127.0.0.1:80 ... #foo = bar ... snowman β˜ƒ ... ''') {'goat': '🐐', 'host:port': '127.0.0.1:80', 'key': 'value', 'snowman': 'β˜ƒ'} Dump some properties to a file and read them back in again:: >>> with open('example.properties', 'w', encoding='latin-1') as fp: ... javaproperties.dump(properties, fp) ... >>> with open('example.properties', 'r', encoding='latin-1') as fp: ... javaproperties.load(fp) ... {'goat': '🐐', 'host:port': '127.0.0.1:80', 'key': 'value', 'snowman': 'β˜ƒ'} Sort the properties you're dumping:: >>> print(javaproperties.dumps(properties, sort_keys=True)) #Mon Sep 26 14:57:44 EDT 2016 goat=\ud83d\udc10 host\:port=127.0.0.1\:80 key=value snowman=\u2603 Turn off the timestamp:: >>> print(javaproperties.dumps(properties, timestamp=None)) key=value goat=\ud83d\udc10 host\:port=127.0.0.1\:80 snowman=\u2603 Use your own timestamp (automatically converted to local time):: >>> print(javaproperties.dumps(properties, timestamp=1234567890)) #Fri Feb 13 18:31:30 EST 2009 key=value goat=\ud83d\udc10 host\:port=127.0.0.1\:80 snowman=\u2603 Dump as XML:: >>> print(javaproperties.dumps_xml(properties)) value 🐐 127.0.0.1:80 β˜ƒ New in v0.6.0: Dump Unicode characters as-is instead of escaping them:: >>> print(javaproperties.dumps(properties, ensure_ascii=False)) #Tue Feb 25 19:13:27 EST 2020 key=value goat=🐐 host\:port=127.0.0.1\:80 snowman=β˜ƒ `And more! `_ .. |properties| replace:: Java ``.properties`` files .. _properties: https://en.wikipedia.org/wiki/.properties .. |propclass| replace:: Java 8's ``java.util.Properties`` .. _propclass: https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html .. |clipkg| replace:: ``javaproperties-cli`` .. _clipkg: https://github.com/jwodder/javaproperties-cli javaproperties-0.6.0/TODO.md000066400000000000000000000050151362623413500156500ustar00rootroot00000000000000- Write tests - Test reading & writing bytes in both Python 2 and Python 3 - Run doctest on the README examples somehow? - Test `parse()` (primarily handling of comments/blanks, repeated keys, and backslash at EOF) - Test `dump()` and `load()` - Documentation: - Add docstrings for the private functions - Try to include the line number (and column number and filename?) in `InvalidUEscapeError`s - Update `Properties` to match the latest Java version? New Features ------------ - `dump()`: Support `str`s as input in Python 2 - Use `unicode_literals` less? - Give `load` and `loads` a `timestamp_hook` argument for specifying a callable to pass the file's timestamp (if any) to - The timestamp is passed as an unparsed string with leading `#` and trailing newline (and other whitespace?) removed - Make `parse()` accept strings as input - Add an equivalent of `parse()` for XML that can extract the comment? - Export `getproperties` and `setproperties` from `javaproperties-cli`? - Make `parse` return a generator of `KeyValue`, `Whitespace`, and `Comment` objects? - Give `Comment` an `is_timestamp()` method - All objects have a `source: str` attribute (including trailing newline) - Give `Comment` an attribute for getting the text without the leading `#` or `!` (and with escapes unescaped?) ? - `KeyValue` objects have `key` and `value` attributes storing the unescaped values - Give `KeyValue` objects an attribute for whether they end with a trailing line continuation? / an attribute for the `source` without any trailing continuations (and also without trailing newline?) ? - `PropertiesFile`: - Support getting, setting (including in `dump`), & deleting the timestamp - Use the last timestamp-like line as the value of the timestamp when getting but the first timestamp-like line for the location when setting, like is done for repeated keys? - Support XML - Support getting, setting, & deleting comments - Support inserting key-value pairs at specific locations? - Support concatenating two `PropertiesFile`s? - Add an option for preserving the separator used in the input when overwriting a key-value pair? - Should instances stringify to their `dump` representations? - Should `Properties` instances stringify to their `dump` representations? - Give `escape()` an option (named `is_value`? `escape_spaces`?) for controlling whether to perform the more minimal escaping used for values rather than keys javaproperties-0.6.0/docs/000077500000000000000000000000001362623413500155105ustar00rootroot00000000000000javaproperties-0.6.0/docs/changelog.rst000066400000000000000000000051221362623413500201710ustar00rootroot00000000000000.. currentmodule:: javaproperties Changelog ========= v0.6.0 (2020-02-28) ------------------- - Include changelog in the Read the Docs site - Support Python 3.8 - When dumping a value that begins with more than one space, only escape the first space in order to better match Java's behavior - Gave `dump()`, `dumps()`, `escape()`, and `join_key_value()` an ``ensure_ascii`` parameter for optionally not escaping non-ASCII characters in output - Gave `dump()` and `dumps()` an ``ensure_ascii_comments`` parameter for controlling what characters in the ``comments`` parameter are escaped - Gave `to_comment()` an ``ensure_ascii`` parameter for controlling what characters are escaped - Added a custom encoding error handler ``'javapropertiesreplace'`` that encodes invalid characters as ``\uXXXX`` escape sequences v0.5.2 (2019-04-08) ------------------- - Added an example of each format to the format descriptions in the docs - Fix building in non-UTF-8 environments v0.5.1 (2018-10-25) ------------------- - **Bugfix**: `java_timestamp()` now properly handles naΓ―ve `~datetime.datetime` objects with ``fold=1`` - Include installation instructions, examples, and GitHub links in the Read the Docs site v0.5.0 (2018-09-18) ------------------- - **Breaking**: Invalid ``\uXXXX`` escape sequences now cause an `InvalidUEscapeError` to be raised - `Properties` instances can now compare equal to `dict`\s and other mapping types - Gave `Properties` a ``copy`` method - Drop support for Python 2.6 and 3.3 - Fixed a `DeprecationWarning` in Python 3.7 v0.4.0 (2017-04-22) ------------------- - Split off the command-line programs into a separate package, |clipkg|_ .. |clipkg| replace:: ``javaproperties-cli`` .. _clipkg: https://github.com/jwodder/javaproperties-cli v0.3.0 (2017-04-13) ------------------- - Added the `PropertiesFile` class for preserving comments in files [#1] - The ``ordereddict`` package is now required under Python 2.6 v0.2.1 (2017-03-20) ------------------- - **Bugfix** to :program:`javaproperties` command: Don't die horribly on missing non-ASCII keys - PyPy now supported v0.2.0 (2016-11-14) ------------------- - Added a :program:`javaproperties` command for basic command-line manipulating of ``.properties`` files - Gave :program:`json2properties` a ``--separator`` option - Gave :program:`json2properties` and :program:`properties2json` ``--encoding`` options - Exported the `java_timestamp()` function - `to_comment()` now converts CR LF and CR line endings inside comments to LF - Some minor documentation improvements v0.1.0 (2016-10-02) ------------------- Initial release javaproperties-0.6.0/docs/cli.rst000066400000000000000000000006221362623413500170110ustar00rootroot00000000000000Command-Line Utilities ====================== As of version 0.4.0, the command-line programs have been split off into a separate package, |clipkg|_, which must be installed separately in order to use them. See `the package's documentation `_ for details. .. |clipkg| replace:: ``javaproperties-cli`` .. _clipkg: https://github.com/jwodder/javaproperties-cli javaproperties-0.6.0/docs/conf.py000066400000000000000000000023101362623413500170030ustar00rootroot00000000000000from javaproperties import __version__ project = 'javaproperties' author = 'John T. Wodder II' copyright = '2016-2020 John T. Wodder II' extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode', ] autodoc_default_options = { 'members': True, 'undoc-members': True, } # NOTE: Do not set 'inherited-members', as it will cause all of the # MutableMapping methods to be listed under `Properties`. intersphinx_mapping = { "python": ("https://docs.python.org/3", None), } exclude_patterns = ['_build'] source_suffix = '.rst' source_encoding = 'utf-8-sig' master_doc = 'index' version = __version__ release = __version__ today_fmt = '%Y %b %d' default_role = 'py:obj' pygments_style = 'sphinx' todo_include_todos = True rst_epilog = ''' .. |py2str| replace:: `!str` .. _py2str: https://docs.python.org/2/library/functions.html#str .. |unicode| replace:: `unicode` .. _unicode: https://docs.python.org/2/library/functions.html#unicode ''' html_theme = 'sphinx_rtd_theme' html_theme_options = { "collapse_navigation": False, } html_last_updated_fmt = '%Y %b %d' html_show_sourcelink = True html_show_sphinx = True html_show_copyright = True javaproperties-0.6.0/docs/index.rst000066400000000000000000000101221362623413500173450ustar00rootroot00000000000000.. module:: javaproperties ============================================================== javaproperties β€” Read & write Java .properties files in Python ============================================================== `GitHub `_ | `PyPI `_ | `Documentation `_ | `Issues `_ | :doc:`Changelog ` .. toctree:: :hidden: plain xmlprops propclass propfile util cli changelog `javaproperties` provides support for reading & writing |properties|_ (both the simple line-oriented format and XML) with a simple API based on the `json` module β€” though, for recovering Java addicts, it also includes a `Properties` class intended to match the behavior of |java8properties|_ as much as is Pythonically possible. Previous versions of `javaproperties` included command-line programs for basic manipulation of ``.properties`` files. As of version 0.4.0, these programs have been split off into a separate package, |clipkg|_. .. note:: Throughout the documentation, "text string" means a Unicode character string β€” |unicode|_ in Python 2, `str` in Python 3. Installation ============ Just use `pip `_ (You have pip, right?) to install ``javaproperties`` and its dependencies:: pip install javaproperties Examples ======== Dump some keys & values (output order not guaranteed):: >>> properties = {"key": "value", "host:port": "127.0.0.1:80", "snowman": "β˜ƒ", "goat": "🐐"} >>> print(javaproperties.dumps(properties)) #Mon Sep 26 14:57:44 EDT 2016 key=value goat=\ud83d\udc10 host\:port=127.0.0.1\:80 snowman=\u2603 Load some keys & values:: >>> javaproperties.loads(''' ... #Mon Sep 26 14:57:44 EDT 2016 ... key = value ... goat: \\ud83d\\udc10 ... host\\:port=127.0.0.1:80 ... #foo = bar ... snowman β˜ƒ ... ''') {'goat': '🐐', 'host:port': '127.0.0.1:80', 'key': 'value', 'snowman': 'β˜ƒ'} Dump some properties to a file and read them back in again:: >>> with open('example.properties', 'w', encoding='latin-1') as fp: ... javaproperties.dump(properties, fp) ... >>> with open('example.properties', 'r', encoding='latin-1') as fp: ... javaproperties.load(fp) ... {'goat': '🐐', 'host:port': '127.0.0.1:80', 'key': 'value', 'snowman': 'β˜ƒ'} Sort the properties you're dumping:: >>> print(javaproperties.dumps(properties, sort_keys=True)) #Mon Sep 26 14:57:44 EDT 2016 goat=\ud83d\udc10 host\:port=127.0.0.1\:80 key=value snowman=\u2603 Turn off the timestamp:: >>> print(javaproperties.dumps(properties, timestamp=None)) key=value goat=\ud83d\udc10 host\:port=127.0.0.1\:80 snowman=\u2603 Use your own timestamp (automatically converted to local time):: >>> print(javaproperties.dumps(properties, timestamp=1234567890)) #Fri Feb 13 18:31:30 EST 2009 key=value goat=\ud83d\udc10 host\:port=127.0.0.1\:80 snowman=\u2603 Dump as XML:: >>> print(javaproperties.dumps_xml(properties)) value 🐐 127.0.0.1:80 β˜ƒ New in v0.6.0: Dump Unicode characters as-is instead of escaping them:: >>> print(javaproperties.dumps(properties, ensure_ascii=False)) #Tue Feb 25 19:13:27 EST 2020 key=value goat=🐐 host\:port=127.0.0.1\:80 snowman=β˜ƒ Indices and tables ================== * :ref:`genindex` * :ref:`search` .. |properties| replace:: Java ``.properties`` files .. _properties: https://en.wikipedia.org/wiki/.properties .. |java8properties| replace:: Java 8's ``java.util.Properties`` .. _java8properties: https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html .. |clipkg| replace:: ``javaproperties-cli`` .. _clipkg: http://javaproperties-cli.readthedocs.io javaproperties-0.6.0/docs/plain.rst000066400000000000000000000105361362623413500173520ustar00rootroot00000000000000.. currentmodule:: javaproperties Simple Line-Oriented ``.properties`` Format =========================================== Format Overview --------------- The simple line-oriented ``.properties`` file format consists of a series of key-value string pairs, one (or fewer) per line, with the key & value separated by the first occurrence of an equals sign (``=``, optionally with surrounding whitespace), a colon (``:``, optionally with surrounding whitespace), or non-leading whitespace. A line without a separator is treated as a key whose value is the empty string. If the same key occurs more than once in a single file, only its last value is used. .. note:: Lines are terminated by ``\n`` (LF), ``\r\n`` (CR LF), or ``\r`` (CR). .. note:: For the purposes of this format, only the space character (ASCII 0x20), the tab character (ASCII 0x09), and the form feed character (ASCII 0x0C) count as whitespace. Leading whitespace on a line is ignored, but trailing whitespace (after stripping trailing newlines) is not. Lines whose first non-whitespace character is ``#`` or ``!`` (not escaped) are comments and are ignored. Entries can be extended across multiple lines by ending all but the last line with a backslash; the backslash, the line ending after it, and any leading whitespace on the next line will all be discarded. A backslash at the end of a comment line has no effect. A comment line after a line that ends with a backslash is treated as part of a normal key-value entry, not as a comment. Occurrences of ``=``, ``:``, ``#``, ``!``, and whitespace inside a key or value are escaped with a backslash. In addition, the following escape sequences are recognized:: \t \n \f \r \uXXXX \\ Unicode characters outside the Basic Multilingual Plane can be represented by a pair of ``\uXXXX`` escape sequences encoding the corresponding UTF-16 surrogate pair. If a backslash is followed by character other than those listed above, the backslash is discarded. An example simple line-oriented ``.properties`` file: .. code-block:: properties #This is a comment. foo=bar baz: quux gnusto cleesh snowman = \u2603 goat = \ud83d\udc10 novalue host\:port=127.0.0.1\:80 This corresponds to the Python `dict`: .. code-block:: python { "foo": "bar", "baz": "quux", "gnusto": "cleesh", "snowman": "β˜ƒ", "goat": "🐐", "novalue": "", "host:port": "127.0.0.1:80", } File Encoding ------------- Although the `load()` and `loads()` functions accept arbitrary Unicode characters in their input, by default the `dump()` and `dumps()` functions limit the characters in their output as follows: - When ``ensure_ascii`` is `True` (the default), `dump()` and `dumps()` output keys & values in pure ASCII; non-ASCII and unprintable characters are escaped with the escape sequences listed above. When ``ensure_ascii`` is `False`, the functions instead pass all non-ASCII characters through as-is; unprintable characters are still escaped. - When ``ensure_ascii_comments`` is `None` (the default), `dump()` and `dumps()` output the ``comments`` argument (if set) using only Latin-1 (ISO-8859-1) characters; all other characters are escaped. When ``ensure_ascii_comments`` is `True`, the functions instead escape all non-ASCII characters in ``comments``. When ``ensure_ascii_comments`` is `False`, the functions instead pass all characters in ``comments`` through as-is. - Note that, in order to match the behavior of Java's ``Properties`` class, unprintable ASCII characters in ``comments`` are always passed through as-is rather than escaped. - Newlines inside ``comments`` are not escaped, but a ``#`` is inserted after every one not already followed by a ``#`` or ``!``. When writing properties to a file, you must either (a) open the file using an encoding that supports all of the characters in the formatted output or else (b) open the file using the :ref:`'javapropertiesreplace' error handler ` defined by this module. The latter option allows one to write valid simple-format properties files in any encoding without having to worry about whether the properties or comment contain any characters not representable in the encoding. Functions --------- .. autofunction:: dump .. autofunction:: dumps .. autofunction:: load .. autofunction:: loads javaproperties-0.6.0/docs/propclass.rst000066400000000000000000000001471362623413500202520ustar00rootroot00000000000000.. currentmodule:: javaproperties ``Properties`` Class ==================== .. autoclass:: Properties javaproperties-0.6.0/docs/propfile.rst000066400000000000000000000001631362623413500200620ustar00rootroot00000000000000.. currentmodule:: javaproperties ``PropertiesFile`` Class ======================== .. autoclass:: PropertiesFile javaproperties-0.6.0/docs/requirements.txt000066400000000000000000000000441362623413500207720ustar00rootroot00000000000000Sphinx~=2.0 sphinx_rtd_theme~=0.4.0 javaproperties-0.6.0/docs/util.rst000066400000000000000000000036211362623413500172210ustar00rootroot00000000000000.. currentmodule:: javaproperties Low-Level Utilities =================== .. autofunction:: escape .. autofunction:: java_timestamp .. autofunction:: join_key_value .. autofunction:: parse .. autofunction:: to_comment .. autofunction:: unescape .. autoexception:: InvalidUEscapeError :show-inheritance: .. _javapropertiesreplace: .. index:: single: javapropertiesreplace Custom Encoding Error Handler ----------------------------- .. versionadded:: 0.6.0 Importing `javaproperties` causes a custom error handler, ``'javapropertiesreplace'``, to be automatically defined that can then be supplied as the *errors* argument to `str.encode`, `open`, or similar encoding operations in order to cause all unencodable characters to be replaced by ``\uXXXX`` escape sequences (with non-BMP characters converted to surrogate pairs first). This is useful, for example, when calling ``javaproperties.dump(obj, fp, ensure_ascii=False)`` where ``fp`` has been opened using an encoding that does not contain all Unicode characters (e.g., Latin-1); in such a case, if ``errors='javapropertiesreplace'`` is supplied when opening ``fp``, then any characters in a key or value of ``obj`` that exist outside ``fp``'s character set will be safely encoded as ``.properties`` file format-compatible escape sequences instead of raising an error. Note that the hexadecimal value used in a ``\uXXXX`` escape sequences is always based on the source character's codepoint value in Unicode regardless of the target encoding:: >>> # Here we see one character encoded to the byte 0x00f0 (because that's >>> # how the target encoding represents it) and a completely different >>> # character encoded as the escape sequence \u00f0 (because that's its >>> # value in Unicode): >>> 'apple: \uF8FF; edh: \xF0'.encode('mac_roman', 'javapropertiesreplace') b'apple: \xf0; edh: \\u00f0' .. autofunction:: javapropertiesreplace_errors javaproperties-0.6.0/docs/xmlprops.rst000066400000000000000000000023651362623413500201340ustar00rootroot00000000000000.. currentmodule:: javaproperties XML ``.properties`` Format ========================== Format Overview --------------- The XML ``.properties`` file format encodes a series of key-value string pairs (and optionally also a comment) as an XML document conforming to the following Document Type Definition (published at ): .. code-block:: dtd An example XML ``.properties`` file: .. code-block:: xml This is a comment. bar β˜ƒ 🐐 127.0.0.1:80 This corresponds to the Python `dict`: .. code-block:: python { "foo": "bar", "snowman": "β˜ƒ", "goat": "🐐", "host:port": "127.0.0.1:80", } Functions --------- .. autofunction:: dump_xml .. autofunction:: dumps_xml .. autofunction:: load_xml .. autofunction:: loads_xml javaproperties-0.6.0/javaproperties/000077500000000000000000000000001362623413500176165ustar00rootroot00000000000000javaproperties-0.6.0/javaproperties/__init__.py000066400000000000000000000030331362623413500217260ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Read & write Java .properties files ``javaproperties`` provides support for reading & writing Java ``.properties`` files (both the simple line-oriented format and XML) with a simple API based on the ``json`` module β€” though, for recovering Java addicts, it also includes a ``Properties`` class intended to match the behavior of Java 8's ``java.util.Properties`` as much as is Pythonically possible. Visit or for more information. """ from .propclass import Properties from .propfile import PropertiesFile from .reading import InvalidUEscapeError, load, loads, parse, unescape from .writing import dump, dumps, java_timestamp, \ javapropertiesreplace_errors, join_key_value, escape, \ to_comment from .xmlprops import load_xml, loads_xml, dump_xml, dumps_xml __version__ = '0.6.0' __author__ = 'John Thorvald Wodder II' __author_email__ = 'javaproperties@varonathe.org' __license__ = 'MIT' __url__ = 'https://github.com/jwodder/javaproperties' __all__ = [ 'InvalidUEscapeError', 'Properties', 'PropertiesFile', 'dump', 'dump_xml', 'dumps', 'dumps_xml', 'escape', 'java_timestamp', 'javapropertiesreplace_errors', 'join_key_value', 'load', 'load_xml', 'loads', 'loads_xml', 'parse', 'to_comment', 'unescape', ] import codecs codecs.register_error('javapropertiesreplace', javapropertiesreplace_errors) javaproperties-0.6.0/javaproperties/propclass.py000066400000000000000000000210701362623413500221760ustar00rootroot00000000000000# -*- coding: utf-8 -*- from six import PY2, string_types from .reading import load from .writing import dump from .xmlprops import load_xml, dump_xml if PY2: from collections import Mapping, MutableMapping else: from collections.abc import Mapping, MutableMapping _type_err = 'Keys & values of Properties instances must be strings' class Properties(MutableMapping): """ A port of |java8properties|_ that tries to match its behavior as much as is Pythonically possible. `Properties` behaves like a normal `~collections.abc.MutableMapping` class (i.e., you can do ``props[key] = value`` and so forth), except that it may only be used to store strings (|py2str|_ and |unicode|_ in Python 2; just `str` in Python 3). Attempts to use a non-string object as a key or value will produce a `TypeError`. Two `Properties` instances compare equal iff both their key-value pairs and :attr:`defaults` attributes are equal. When comparing a `Properties` instance to any other type of mapping, only the key-value pairs are considered. .. versionchanged:: 0.5.0 `Properties` instances can now compare equal to `dict`\\ s and other mapping types :param data: A mapping or iterable of ``(key, value)`` pairs with which to initialize the `Properties` instance. All keys and values in ``data`` must be text strings. :type data: mapping or `None` :param defaults: a set of default properties that will be used as fallback for `getProperty` :type defaults: `Properties` or `None` .. |java8properties| replace:: Java 8's ``java.util.Properties`` .. _java8properties: https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html """ def __init__(self, data=None, defaults=None): self.data = {} #: A `Properties` subobject used as fallback for `getProperty`. Only #: `getProperty`, `propertyNames`, `stringPropertyNames`, and `__eq__` #: use this attribute; all other methods (including the standard #: mapping methods) ignore it. self.defaults = defaults if data is not None: self.update(data) def __getitem__(self, key): if not isinstance(key, string_types): raise TypeError(_type_err) return self.data[key] def __setitem__(self, key, value): if not isinstance(key, string_types) or \ not isinstance(value, string_types): raise TypeError(_type_err) self.data[key] = value def __delitem__(self, key): if not isinstance(key, string_types): raise TypeError(_type_err) del self.data[key] def __iter__(self): return iter(self.data) def __len__(self): return len(self.data) def __repr__(self): return '{0.__module__}.{0.__name__}({1.data!r}, defaults={1.defaults!r})'\ .format(type(self), self) def __eq__(self, other): if isinstance(other, Properties): return self.data == other.data and self.defaults == other.defaults elif isinstance(other, Mapping): return dict(self) == other else: return NotImplemented def __ne__(self, other): return not (self == other) def getProperty(self, key, defaultValue=None): """ Fetch the value associated with the key ``key`` in the `Properties` instance. If the key is not present, `defaults` is checked, and then *its* `defaults`, etc., until either a value for ``key`` is found or the next `defaults` is `None`, in which case `defaultValue` is returned. :param key: the key to look up the value of :type key: text string :param defaultValue: the value to return if ``key`` is not found in the `Properties` instance :rtype: text string (if ``key`` was found) :raises TypeError: if ``key`` is not a string """ try: return self[key] except KeyError: if self.defaults is not None: return self.defaults.getProperty(key, defaultValue) else: return defaultValue def load(self, inStream): """ Update the `Properties` instance with the entries in a ``.properties`` file or file-like object. ``inStream`` may be either a text or binary filehandle, with or without universal newlines enabled. If it is a binary filehandle, its contents are decoded as Latin-1. .. versionchanged:: 0.5.0 Invalid ``\\uXXXX`` escape sequences will now cause an `InvalidUEscapeError` to be raised :param inStream: the file from which to read the ``.properties`` document :type inStream: file-like object :return: `None` :raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence occurs in the input """ self.data.update(load(inStream)) def propertyNames(self): r""" Returns a generator of all distinct keys in the `Properties` instance and its `defaults` (and its `defaults`\ ’s `defaults`, etc.) in unspecified order :rtype: generator of text strings """ for k in self.data: yield k if self.defaults is not None: for k in self.defaults.propertyNames(): if k not in self.data: yield k def setProperty(self, key, value): """ Equivalent to ``self[key] = value`` """ self[key] = value def store(self, out, comments=None): """ Write the `Properties` instance's entries (in unspecified order) in ``.properties`` format to ``out``, including the current timestamp. :param out: A file-like object to write the properties to. It must have been opened as a text file with a Latin-1-compatible encoding. :param comments: If non-`None`, ``comments`` will be written to ``out`` as a comment before any other content :type comments: text string or `None` :return: `None` """ dump(self.data, out, comments=comments) def stringPropertyNames(self): r""" Returns a `set` of all keys in the `Properties` instance and its `defaults` (and its `defaults`\ ’s `defaults`, etc.) :rtype: `set` of text strings """ names = set(self.data) if self.defaults is not None: names.update(self.defaults.stringPropertyNames()) return names def loadFromXML(self, inStream): """ Update the `Properties` instance with the entries in the XML properties file ``inStream``. Beyond basic XML well-formedness, `loadFromXML` only checks that the root element is named ``properties`` and that all of its ``entry`` children have ``key`` attributes; no further validation is performed. .. note:: This uses `xml.etree.ElementTree` for parsing, which does not have decent support for |unicode|_ input in Python 2. Files containing non-ASCII characters need to be opened in binary mode in Python 2, while Python 3 accepts both binary and text input. :param inStream: the file from which to read the XML properties document :type inStream: file-like object :return: `None` :raises ValueError: if the root of the XML tree is not a ```` tag or an ```` element is missing a ``key`` attribute """ self.data.update(load_xml(inStream)) def storeToXML(self, out, comment=None, encoding='UTF-8'): """ Write the `Properties` instance's entries (in unspecified order) in XML properties format to ``out``. :param out: a file-like object to write the properties to :type out: binary file-like object :param comment: if non-`None`, ``comment`` will be output as a ```` element before the ```` elements :type comment: text string or `None` :param string encoding: the name of the encoding to use for the XML document (also included in the XML declaration) :return: `None` """ dump_xml(self.data, out, comment=comment, encoding=encoding) def copy(self): """ .. versionadded:: 0.5.0 Create a shallow copy of the mapping. The copy's `defaults` attribute will be the same instance as the original's `defaults`. """ return type(self)(self.data, self.defaults) javaproperties-0.6.0/javaproperties/propfile.py000066400000000000000000000303571362623413500220200ustar00rootroot00000000000000from __future__ import print_function from collections import OrderedDict, namedtuple import six from .reading import loads, parse from .util import CONTINUED_RGX from .writing import join_key_value if six.PY2: from collections import Mapping, MutableMapping else: from collections.abc import Mapping, MutableMapping _type_err = 'Keys & values of PropertiesFile instances must be strings' PropertyLine = namedtuple('PropertyLine', 'key value source') class PropertiesFile(MutableMapping): """ .. versionadded:: 0.3.0 A custom mapping class for reading from, editing, and writing to a ``.properties`` file while preserving comments & whitespace in the original input. A `PropertiesFile` instance can be constructed from another mapping and/or iterable of pairs, after which it will act like an `~collections.OrderedDict`. Alternatively, an instance can be constructed from a file or string with `PropertiesFile.load()` or `PropertiesFile.loads()`, and the resulting instance will remember the formatting of its input and retain that formatting when written back to a file or string with the `~PropertiesFile.dump()` or `~PropertiesFile.dumps()` method. The formatting information attached to an instance ``pf`` can be forgotten by constructing another mapping from it via ``dict(pf)``, ``OrderedDict(pf)``, or even ``PropertiesFile(pf)`` (Use the `copy()` method if you want to create another `PropertiesFile` instance with the same data & formatting). When not reading or writing, `PropertiesFile` behaves like a normal `~collections.abc.MutableMapping` class (i.e., you can do ``props[key] = value`` and so forth), except that (a) like `~collections.OrderedDict`, key insertion order is remembered and is used when iterating & dumping (and `reversed` is supported), and (b) like `Properties`, it may only be used to store strings and will raise a `TypeError` if passed a non-string object as key or value. Two `PropertiesFile` instances compare equal iff both their key-value pairs and comment & whitespace lines are equal and in the same order. When comparing a `PropertiesFile` to any other type of mapping, only the key-value pairs are considered, and order is ignored. `PropertiesFile` currently only supports reading & writing the simple line-oriented format, not XML. """ def __init__(self, mapping=None, **kwargs): #: mapping from keys to list of line numbers self._indices = OrderedDict() #: mapping from line numbers to (key, value, source) tuples self._lines = OrderedDict() if mapping is not None: self.update(mapping) self.update(kwargs) def _check(self): """ Assert the internal consistency of the instance's data structures. This method is for debugging only. """ for k,ix in six.iteritems(self._indices): assert k is not None, 'null key' assert ix, 'Key does not map to any indices' assert ix == sorted(ix), "Key's indices are not in order" for i in ix: assert i in self._lines, 'Key index does not map to line' assert self._lines[i].key is not None, 'Key maps to comment' assert self._lines[i].key == k, 'Key does not map to itself' assert self._lines[i].value is not None, 'Key has null value' prev = None for i, line in six.iteritems(self._lines): assert prev is None or prev < i, 'Line indices out of order' prev = i if line.key is None: assert line.value is None, 'Comment/blank has value' assert line.source is not None, 'Comment source not stored' assert loads(line.source) == {}, 'Comment source is not comment' else: assert line.value is not None, 'Key has null value' if line.source is not None: assert loads(line.source) == {line.key: line.value}, \ 'Key source does not deserialize to itself' assert line.key in self._indices, 'Key is missing from map' assert i in self._indices[line.key], \ 'Key does not map to itself' def __getitem__(self, key): if not isinstance(key, six.string_types): raise TypeError(_type_err) return self._lines[self._indices[key][-1]].value def __setitem__(self, key, value): if not isinstance(key, six.string_types) or \ not isinstance(value, six.string_types): raise TypeError(_type_err) try: ixes = self._indices[key] except KeyError: try: lasti = next(reversed(self._lines)) except StopIteration: ix = 0 else: ix = lasti + 1 # We're adding a line to the end of the file, so make sure the # line before it ends with a newline and (if it's not a # comment) doesn't end with a trailing line continuation. lastline = self._lines[lasti] if lastline.source is not None: lastsrc = lastline.source if lastline.key is not None: lastsrc = CONTINUED_RGX.sub(r'\1', lastsrc) if not lastsrc.endswith(('\r', '\n')): lastsrc += '\n' self._lines[lasti] = lastline._replace(source=lastsrc) else: # Update the first occurrence of the key and discard the rest. # This way, the order in which the keys are listed in the file and # dict will be preserved. ix = ixes.pop(0) for i in ixes: del self._lines[i] self._indices[key] = [ix] self._lines[ix] = PropertyLine(key, value, None) def __delitem__(self, key): if not isinstance(key, six.string_types): raise TypeError(_type_err) for i in self._indices.pop(key): del self._lines[i] def __iter__(self): return iter(self._indices) def __reversed__(self): return reversed(self._indices) def __len__(self): return len(self._indices) def _comparable(self): return [ (None, line.source) if line.key is None else (line.key, line.value) for i, line in six.iteritems(self._lines) ### TODO: Also include non-final repeated keys??? if line.key is None or self._indices[line.key][-1] == i ] def __eq__(self, other): if isinstance(other, PropertiesFile): return self._comparable() == other._comparable() ### TODO: Special-case OrderedDict? elif isinstance(other, Mapping): return dict(self) == other else: return NotImplemented def __ne__(self, other): return not (self == other) @classmethod def load(cls, fp): """ Parse the contents of the `~io.IOBase.readline`-supporting file-like object ``fp`` as a simple line-oriented ``.properties`` file and return a `PropertiesFile` instance. ``fp`` may be either a text or binary filehandle, with or without universal newlines enabled. If it is a binary filehandle, its contents are decoded as Latin-1. .. versionchanged:: 0.5.0 Invalid ``\\uXXXX`` escape sequences will now cause an `InvalidUEscapeError` to be raised :param fp: the file from which to read the ``.properties`` document :type fp: file-like object :rtype: PropertiesFile :raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence occurs in the input """ obj = cls() for i, (k, v, src) in enumerate(parse(fp)): if k is not None: obj._indices.setdefault(k, []).append(i) obj._lines[i] = PropertyLine(k, v, src) return obj @classmethod def loads(cls, s): """ Parse the contents of the string ``s`` as a simple line-oriented ``.properties`` file and return a `PropertiesFile` instance. ``s`` may be either a text string or bytes string. If it is a bytes string, its contents are decoded as Latin-1. .. versionchanged:: 0.5.0 Invalid ``\\uXXXX`` escape sequences will now cause an `InvalidUEscapeError` to be raised :param string s: the string from which to read the ``.properties`` document :rtype: PropertiesFile :raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence occurs in the input """ if isinstance(s, six.binary_type): fp = six.BytesIO(s) else: fp = six.StringIO(s) return cls.load(fp) def dump(self, fp, separator='='): """ Write the mapping to a file in simple line-oriented ``.properties`` format. If the instance was originally created from a file or string with `PropertiesFile.load()` or `PropertiesFile.loads()`, then the output will include the comments and whitespace from the original input, and any keys that haven't been deleted or reassigned will retain their original formatting and multiplicity. Key-value pairs that have been modified or added to the mapping will be reformatted with `join_key_value()` using the given separator. All key-value pairs are output in the order they were defined, with new keys added to the end. .. note:: Serializing a `PropertiesFile` instance with the :func:`dump()` function instead will cause all formatting information to be ignored, as :func:`dump()` will treat the instance like a normal mapping. :param fp: A file-like object to write the mapping to. It must have been opened as a text file with a Latin-1-compatible encoding. :param separator: The string to use for separating new or modified keys & values. Only ``" "``, ``"="``, and ``":"`` (possibly with added whitespace) should ever be used as the separator. :type separator: text string :return: `None` """ ### TODO: Support setting the timestamp for line in six.itervalues(self._lines): if line.source is None: print(join_key_value(line.key, line.value, separator), file=fp) else: fp.write(line.source) def dumps(self, separator='='): """ Convert the mapping to a text string in simple line-oriented ``.properties`` format. If the instance was originally created from a file or string with `PropertiesFile.load()` or `PropertiesFile.loads()`, then the output will include the comments and whitespace from the original input, and any keys that haven't been deleted or reassigned will retain their original formatting and multiplicity. Key-value pairs that have been modified or added to the mapping will be reformatted with `join_key_value()` using the given separator. All key-value pairs are output in the order they were defined, with new keys added to the end. .. note:: Serializing a `PropertiesFile` instance with the :func:`dumps()` function instead will cause all formatting information to be ignored, as :func:`dumps()` will treat the instance like a normal mapping. :param separator: The string to use for separating new or modified keys & values. Only ``" "``, ``"="``, and ``":"`` (possibly with added whitespace) should ever be used as the separator. :type separator: text string :rtype: text string """ s = six.StringIO() self.dump(s, separator=separator) return s.getvalue() def copy(self): """ Create a copy of the mapping, including formatting information """ dup = type(self)() dup._indices = OrderedDict( (k, list(v)) for k,v in six.iteritems(self._indices) ) dup._lines = self._lines.copy() return dup javaproperties-0.6.0/javaproperties/reading.py000066400000000000000000000172131362623413500216050ustar00rootroot00000000000000from __future__ import unicode_literals import re from six import binary_type, StringIO, BytesIO, unichr from .util import CONTINUED_RGX, ascii_splitlines def load(fp, object_pairs_hook=dict): """ Parse the contents of the `~io.IOBase.readline`-supporting file-like object ``fp`` as a simple line-oriented ``.properties`` file and return a `dict` of the key-value pairs. ``fp`` may be either a text or binary filehandle, with or without universal newlines enabled. If it is a binary filehandle, its contents are decoded as Latin-1. By default, the key-value pairs extracted from ``fp`` are combined into a `dict` with later occurrences of a key overriding previous occurrences of the same key. To change this behavior, pass a callable as the ``object_pairs_hook`` argument; it will be called with one argument, a generator of ``(key, value)`` pairs representing the key-value entries in ``fp`` (including duplicates) in order of occurrence. `load` will then return the value returned by ``object_pairs_hook``. .. versionchanged:: 0.5.0 Invalid ``\\uXXXX`` escape sequences will now cause an `InvalidUEscapeError` to be raised :param fp: the file from which to read the ``.properties`` document :type fp: file-like object :param callable object_pairs_hook: class or function for combining the key-value pairs :rtype: `dict` of text strings or the return value of ``object_pairs_hook`` :raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence occurs in the input """ return object_pairs_hook((k,v) for k,v,_ in parse(fp) if k is not None) def loads(s, object_pairs_hook=dict): """ Parse the contents of the string ``s`` as a simple line-oriented ``.properties`` file and return a `dict` of the key-value pairs. ``s`` may be either a text string or bytes string. If it is a bytes string, its contents are decoded as Latin-1. By default, the key-value pairs extracted from ``s`` are combined into a `dict` with later occurrences of a key overriding previous occurrences of the same key. To change this behavior, pass a callable as the ``object_pairs_hook`` argument; it will be called with one argument, a generator of ``(key, value)`` pairs representing the key-value entries in ``s`` (including duplicates) in order of occurrence. `loads` will then return the value returned by ``object_pairs_hook``. .. versionchanged:: 0.5.0 Invalid ``\\uXXXX`` escape sequences will now cause an `InvalidUEscapeError` to be raised :param string s: the string from which to read the ``.properties`` document :param callable object_pairs_hook: class or function for combining the key-value pairs :rtype: `dict` of text strings or the return value of ``object_pairs_hook`` :raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence occurs in the input """ fp = BytesIO(s) if isinstance(s, binary_type) else StringIO(s) return load(fp, object_pairs_hook=object_pairs_hook) COMMENT_OR_BLANK_RGX = re.compile(r'^[ \t\f]*(?:[#!]|\r?\n?$)') SEPARATOR_RGX = re.compile(r'(?>> to_comment('They say foo=bar,\\r\\nbut does bar=foo?') '#They say foo=bar,\\n#but does bar=foo?' .. versionchanged:: 0.6.0 ``ensure_ascii`` parameter added :param comment: the string to convert to a comment :type comment: text string :param ensure_ascii: if true, all non-ASCII characters will be replaced with ``\\uXXXX`` escape sequences in the output; if `None`, only non-Latin-1 characters will be escaped; if false, no characters will be escaped :rtype: text string """ comment = NON_N_EOL_RGX.sub('\n', comment) comment = NEWLINE_OLD_COMMENT_RGX.sub('\n#', comment) if ensure_ascii is None: comment = NON_LATIN1_RGX.sub(_esc, comment) elif ensure_ascii: comment = NON_ASCII_RGX.sub(_esc, comment) return '#' + comment def join_key_value(key, value, separator='=', ensure_ascii=True): r""" Join a key and value together into a single line suitable for adding to a simple line-oriented ``.properties`` file. No trailing newline is added. >>> join_key_value('possible separators', '= : space') 'possible\\ separators=\\= \\: space' .. versionchanged:: 0.6.0 ``ensure_ascii`` parameter added :param key: the key :type key: text string :param value: the value :type value: text string :param separator: the string to use for separating the key & value. Only ``" "``, ``"="``, and ``":"`` (possibly with added whitespace) should ever be used as the separator. :type separator: text string :param bool ensure_ascii: if true, all non-ASCII characters will be replaced with ``\\uXXXX`` escape sequences in the output; if false, non-ASCII characters will be passed through as-is :rtype: text string """ # Escapes `key` and `value` the same way as java.util.Properties.store() value = _base_escape(value, ensure_ascii=ensure_ascii) if value.startswith(' '): value = '\\' + value return escape(key, ensure_ascii=ensure_ascii) + separator + value _escapes = { '\t': r'\t', '\n': r'\n', '\f': r'\f', '\r': r'\r', '!': r'\!', '#': r'\#', ':': r'\:', '=': r'\=', '\\': r'\\', } def _esc(m): c = m.group() try: return _escapes[c] except KeyError: return _to_u_escape(c) def _to_u_escape(c): c = ord(c) if c > 0xFFFF: # Does Python really not have a decent builtin way to calculate # surrogate pairs? assert c <= 0x10FFFF c -= 0x10000 return '\\u{0:04x}\\u{1:04x}'.format( 0xD800 + (c >> 10), 0xDC00 + (c & 0x3FF) ) else: return '\\u{0:04x}'.format(c) NEEDS_ESCAPE_ASCII_RGX = re.compile(r'[^\x20-\x7E]|[\\#!=:]') NEEDS_ESCAPE_UNICODE_RGX = re.compile(r'[\x00-\x1F\x7F]|[\\#!=:]') def _base_escape(field, ensure_ascii=True): rgx = NEEDS_ESCAPE_ASCII_RGX if ensure_ascii else NEEDS_ESCAPE_UNICODE_RGX return rgx.sub(_esc, field) def escape(field, ensure_ascii=True): """ Escape a string so that it can be safely used as either a key or value in a ``.properties`` file. All non-ASCII characters, all nonprintable or space characters, and the characters ``\\ # ! = :`` are all escaped using either the single-character escapes recognized by `unescape` (when they exist) or ``\\uXXXX`` escapes (after converting non-BMP characters to surrogate pairs). .. versionchanged:: 0.6.0 ``ensure_ascii`` parameter added :param field: the string to escape :type field: text string :param bool ensure_ascii: if true, all non-ASCII characters will be replaced with ``\\uXXXX`` escape sequences in the output; if false, non-ASCII characters will be passed through as-is :rtype: text string """ return _base_escape(field, ensure_ascii=ensure_ascii).replace(' ', r'\ ') DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] MONTHS = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ] def java_timestamp(timestamp=True): """ .. versionadded:: 0.2.0 Returns a timestamp in the format produced by |date_tostring|_, e.g.:: Mon Sep 02 14:00:54 EDT 2016 If ``timestamp`` is `True` (the default), the current date & time is returned. If ``timestamp`` is `None` or `False`, an empty string is returned. If ``timestamp`` is a number, it is converted from seconds since the epoch to local time. If ``timestamp`` is a `datetime.datetime` object, its value is used directly, with naΓ―ve objects assumed to be in the local timezone. The timestamp is always constructed using the C locale. :param timestamp: the date & time to display :type timestamp: `None`, `bool`, number, or `datetime.datetime` :rtype: text string .. |date_tostring| replace:: Java 8's ``Date.toString()`` .. _date_tostring: https://docs.oracle.com/javase/8/docs/api/java/util/Date.html#toString-- """ if timestamp is None or timestamp is False: return '' if isinstance(timestamp, datetime) and timestamp.tzinfo is not None: timebits = timestamp.timetuple() # Assumes `timestamp.tzinfo.tzname()` is meaningful/useful tzname = timestamp.tzname() else: if timestamp is True: timestamp = None elif isinstance(timestamp, datetime): try: # Use `datetime.timestamp()` if it's available, as it (unlike # `datetime.timetuple()`) takes `fold` into account for naΓ―ve # datetimes timestamp = timestamp.timestamp() except AttributeError: # Pre-Python 3.3 # Mapping `timetuple` through `mktime` and `localtime` is # necessary for determining whether DST is in effect (which, in # turn, is necessary for determining which timezone name to # use). The only downside to using standard functions instead # of `python-dateutil` is that `mktime`, apparently, handles # times duplicated by DST non-deterministically (cf. # ), but there's no right way to deal # with those anyway, so... timestamp = time.mktime(timestamp.timetuple()) elif not isinstance(timestamp, numbers.Number): raise TypeError('Timestamp must be number or datetime.datetime') timebits = time.localtime(timestamp) try: tzname = timebits.tm_zone except AttributeError: # This assumes that `time.tzname` is meaningful/useful. tzname = time.tzname[timebits.tm_isdst > 0] assert 1 <= timebits.tm_mon <= 12, 'invalid month' assert 0 <= timebits.tm_wday <= 6, 'invalid day of week' return '{wday} {mon} {t.tm_mday:02d}' \ ' {t.tm_hour:02d}:{t.tm_min:02d}:{t.tm_sec:02d}' \ ' {tz} {t.tm_year:04d}'.format( t=timebits, tz=tzname, mon=MONTHS[timebits.tm_mon-1], wday=DAYS_OF_WEEK[timebits.tm_wday] ) def javapropertiesreplace_errors(e): """ .. versionadded:: 0.6.0 Implements the ``'javapropertiesreplace'`` error handling (for text encodings only): unencodable characters are replaced by ``\\uXXXX`` escape sequences (with non-BMP characters converted to surrogate pairs first) """ if isinstance(e, UnicodeEncodeError): return (''.join(map(_to_u_escape, e.object[e.start:e.end])), e.end) else: raise e javaproperties-0.6.0/javaproperties/xmlprops.py000066400000000000000000000152251362623413500220610ustar00rootroot00000000000000from __future__ import print_function, unicode_literals import codecs import xml.etree.ElementTree as ET from xml.sax.saxutils import escape, quoteattr from .util import itemize def load_xml(fp, object_pairs_hook=dict): r""" Parse the contents of the file-like object ``fp`` as an XML properties file and return a `dict` of the key-value pairs. Beyond basic XML well-formedness, `load_xml` only checks that the root element is named "``properties``" and that all of its ```` children have ``key`` attributes. No further validation is performed; if any ````\ s happen to contain nested tags, the behavior is undefined. By default, the key-value pairs extracted from ``fp`` are combined into a `dict` with later occurrences of a key overriding previous occurrences of the same key. To change this behavior, pass a callable as the ``object_pairs_hook`` argument; it will be called with one argument, a generator of ``(key, value)`` pairs representing the key-value entries in ``fp`` (including duplicates) in order of occurrence. `load_xml` will then return the value returned by ``object_pairs_hook``. .. note:: This uses `xml.etree.ElementTree` for parsing, which does not have decent support for |unicode|_ input in Python 2. Files containing non-ASCII characters need to be opened in binary mode in Python 2, while Python 3 accepts both binary and text input. :param fp: the file from which to read the XML properties document :type fp: file-like object :param callable object_pairs_hook: class or function for combining the key-value pairs :rtype: `dict` or the return value of ``object_pairs_hook`` :raises ValueError: if the root of the XML tree is not a ```` tag or an ```` element is missing a ``key`` attribute """ tree = ET.parse(fp) return object_pairs_hook(_fromXML(tree.getroot())) def loads_xml(s, object_pairs_hook=dict): r""" Parse the contents of the string ``s`` as an XML properties document and return a `dict` of the key-value pairs. Beyond basic XML well-formedness, `loads_xml` only checks that the root element is named "``properties``" and that all of its ```` children have ``key`` attributes. No further validation is performed; if any ````\ s happen to contain nested tags, the behavior is undefined. By default, the key-value pairs extracted from ``s`` are combined into a `dict` with later occurrences of a key overriding previous occurrences of the same key. To change this behavior, pass a callable as the ``object_pairs_hook`` argument; it will be called with one argument, a generator of ``(key, value)`` pairs representing the key-value entries in ``s`` (including duplicates) in order of occurrence. `loads_xml` will then return the value returned by ``object_pairs_hook``. .. note:: This uses `xml.etree.ElementTree` for parsing, which does not have decent support for |unicode|_ input in Python 2. Strings containing non-ASCII characters need to be encoded as bytes in Python 2 (Use either UTF-8 or UTF-16 if the XML document does not contain an encoding declaration), while Python 3 accepts both binary and text input. :param string s: the string from which to read the XML properties document :param callable object_pairs_hook: class or function for combining the key-value pairs :rtype: `dict` or the return value of ``object_pairs_hook`` :raises ValueError: if the root of the XML tree is not a ```` tag or an ```` element is missing a ``key`` attribute """ elem = ET.fromstring(s) return object_pairs_hook(_fromXML(elem)) def _fromXML(root): if root.tag != 'properties': raise ValueError('XML tree is not rooted at ') for entry in root.findall('entry'): key = entry.get('key') if key is None: raise ValueError(' is missing "key" attribute') yield (key, entry.text) def dump_xml(props, fp, comment=None, encoding='UTF-8', sort_keys=False): """ Write a series ``props`` of key-value pairs to a binary filehandle ``fp`` in the format of an XML properties file. The file will include both an XML declaration and a doctype declaration. :param props: A mapping or iterable of ``(key, value)`` pairs to write to ``fp``. All keys and values in ``props`` must be text strings. If ``sort_keys`` is `False`, the entries are output in iteration order. :param fp: a file-like object to write the values of ``props`` to :type fp: binary file-like object :param comment: if non-`None`, ``comment`` will be output as a ```` element before the ```` elements :type comment: text string or `None` :param string encoding: the name of the encoding to use for the XML document (also included in the XML declaration) :param bool sort_keys: if true, the elements of ``props`` are sorted lexicographically by key in the output :return: `None` """ fp = codecs.lookup(encoding).streamwriter(fp, errors='xmlcharrefreplace') print('' .format(quoteattr(encoding)), file=fp) for s in _stream_xml(props, comment, sort_keys): print(s, file=fp) def dumps_xml(props, comment=None, sort_keys=False): """ Convert a series ``props`` of key-value pairs to a text string containing an XML properties document. The document will include a doctype declaration but not an XML declaration. :param props: A mapping or iterable of ``(key, value)`` pairs to serialize. All keys and values in ``props`` must be text strings. If ``sort_keys`` is `False`, the entries are output in iteration order. :param comment: if non-`None`, ``comment`` will be output as a ```` element before the ```` elements :type comment: text string or `None` :param bool sort_keys: if true, the elements of ``props`` are sorted lexicographically by key in the output :rtype: text string """ return ''.join(s + '\n' for s in _stream_xml(props, comment, sort_keys)) def _stream_xml(props, comment=None, sort_keys=False): yield '' yield '' if comment is not None: yield '' + escape(comment) + '' for k,v in itemize(props, sort_keys=sort_keys): yield '{1}'.format(quoteattr(k), escape(v)) yield '' javaproperties-0.6.0/setup.cfg000066400000000000000000000031561362623413500164060ustar00rootroot00000000000000[aliases] make=sdist bdist_wheel [bdist_wheel] universal=1 [metadata] name = javaproperties #version = # Set in setup.py description = Read & write Java .properties files long_description = file:README.rst long_description_content_type = text/x-rst author = John Thorvald Wodder II author_email = javaproperties@varonathe.org license = MIT license_file = LICENSE url = https://github.com/jwodder/javaproperties keywords = java properties javaproperties configfile config configuration classifiers = Development Status :: 4 - Beta #Development Status :: 5 - Production/Stable Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy License :: OSI Approved :: MIT License Intended Audience :: Developers Topic :: Software Development Topic :: Software Development :: Libraries :: Java Libraries Topic :: Utilities project_urls = Source Code = https://github.com/jwodder/javaproperties Bug Tracker = https://github.com/jwodder/javaproperties/issues Documentation = https://javaproperties.readthedocs.io Say Thanks! = https://saythanks.io/to/jwodder [options] packages = find: python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4 install_requires = six ~= 1.4 javaproperties-0.6.0/setup.py000066400000000000000000000007161362623413500162760ustar00rootroot00000000000000import io from os.path import dirname, join import re from setuptools import setup with io.open( join(dirname(__file__), 'javaproperties', '__init__.py'), encoding='utf-8', ) as fp: for line in fp: m = re.search(r'^\s*__version__\s*=\s*([\'"])([^\'"]+)\1\s*$', line) if m: version = m.group(2) break else: raise RuntimeError('Unable to find own __version__ string') setup(version=version) javaproperties-0.6.0/test/000077500000000000000000000000001362623413500155375ustar00rootroot00000000000000javaproperties-0.6.0/test/test_dump_xml.py000066400000000000000000000016121362623413500207750ustar00rootroot00000000000000from __future__ import unicode_literals import pytest from six import BytesIO from javaproperties import dump_xml # The only thing special about `dump_xml` compared to `dumps_xml` is encoding, # so that's the only thing we'll test here. @pytest.mark.parametrize('enc', ['ASCII', 'Latin-1', 'UTF-16BE', 'UTF-8']) def test_dump_xml_encoding(enc): fp = BytesIO() dump_xml([ ('key', 'value'), ('edh', '\xF0'), ('snowman', '\u2603'), ('goat', '\U0001F410'), ], fp, encoding=enc) assert fp.getvalue() == '''\ value \xF0 \u2603 \U0001F410 '''.format(enc).encode(enc, 'xmlcharrefreplace') javaproperties-0.6.0/test/test_dumps.py000066400000000000000000000144761362623413500203140ustar00rootroot00000000000000from __future__ import unicode_literals from collections import OrderedDict from datetime import datetime from dateutil.tz import tzoffset import pytest from six import unichr from javaproperties import dumps, to_comment @pytest.mark.parametrize('d,s', [ ({}, ''), ({"key": "value"}, 'key=value\n'), ([("key", "value"), ("zebra", "apple")], 'key=value\nzebra=apple\n'), ([("zebra", "apple"), ("key", "value")], 'zebra=apple\nkey=value\n'), ( OrderedDict([("key", "value"), ("zebra", "apple")]), 'key=value\nzebra=apple\n', ), ( OrderedDict([("zebra", "apple"), ("key", "value")]), 'zebra=apple\nkey=value\n', ), ({"two words": "value"}, 'two\\ words=value\n'), ({"key": "two words"}, 'key=two words\n'), ({" key": "value"}, '\\ key=value\n'), ({"key": " value"}, 'key=\\ value\n'), ({"key ": "value"}, 'key\\ =value\n'), ({"key": "value "}, 'key=value \n'), ({" ": "value"}, '\\ \\ \\ =value\n'), ({"key": " "}, 'key=\\ \n'), ({"US": "\x1F"}, 'US=\\u001f\n'), ({"tilde": "~"}, 'tilde=~\n'), ({"delete": "\x7F"}, 'delete=\\u007f\n'), ({"edh": "\xF0"}, 'edh=\\u00f0\n'), ({"snowman": "\u2603"}, 'snowman=\\u2603\n'), ({"goat": "\U0001F410"}, 'goat=\\ud83d\\udc10\n'), ({"taog": "\uDC10\uD83D"}, 'taog=\\udc10\\ud83d\n'), ({"newline": "\n"}, 'newline=\\n\n'), ({"carriage-return": "\r"}, 'carriage-return=\\r\n'), ({"tab": "\t"}, 'tab=\\t\n'), ({"form-feed": "\f"}, 'form-feed=\\f\n'), ({"bell": "\a"}, 'bell=\\u0007\n'), ({"escape": "\x1B"}, 'escape=\\u001b\n'), ({"vertical-tab": "\v"}, 'vertical-tab=\\u000b\n'), ({"backslash": "\\"}, 'backslash=\\\\\n'), ({"equals": "="}, 'equals=\\=\n'), ({"colon": ":"}, 'colon=\\:\n'), ({"hash": "#"}, 'hash=\\#\n'), ({"exclamation": "!"}, 'exclamation=\\!\n'), ({"null": "\0"}, 'null=\\u0000\n'), ({"backspace": "\b"}, 'backspace=\\u0008\n'), ]) def test_dumps(d,s): assert dumps(d, timestamp=False) == s @pytest.mark.parametrize('d,s', [ ({"key": "value", "zebra": "apple"}, 'key=value\nzebra=apple\n'), ([("key", "value"), ("zebra", "apple")], 'key=value\nzebra=apple\n'), ([("zebra", "apple"), ("key", "value")], 'key=value\nzebra=apple\n'), ( OrderedDict([("key", "value"), ("zebra", "apple")]), 'key=value\nzebra=apple\n', ), ( OrderedDict([("zebra", "apple"), ("key", "value")]), 'key=value\nzebra=apple\n', ), ]) def test_dumps_sorted(d,s): assert dumps(d, timestamp=False, sort_keys=True) == s @pytest.mark.parametrize('ts,s', [ (None, 'key=value\n'), (1473703254, '#Mon Sep 12 14:00:54 EDT 2016\nkey=value\n'), ( datetime.fromtimestamp(1473703254), '#Mon Sep 12 14:00:54 EDT 2016\nkey=value\n', ), ( datetime.fromtimestamp(1473703254, tzoffset('PDT', -25200)), '#Mon Sep 12 11:00:54 PDT 2016\nkey=value\n', ), ]) def test_dump_timestamp(ts, s): assert dumps({"key": "value"}, timestamp=ts) == s @pytest.mark.parametrize('d,s', [ ({"US": "\x1F"}, 'US=\\u001f\n'), ({"delete": "\x7F"}, 'delete=\\u007f\n'), ({"padding": "\x80"}, 'padding=\x80\n'), ({"nbsp": "\xA0"}, 'nbsp=\xA0\n'), ({"edh": "\xF0"}, 'edh=\xF0\n'), ({"snowman": "\u2603"}, 'snowman=\u2603\n'), ({"goat": "\U0001F410"}, 'goat=\U0001F410\n'), ({"taog": "\uDC10\uD83D"}, 'taog=\uDC10\uD83D\n'), ({"newline": "\n"}, 'newline=\\n\n'), ({"carriage-return": "\r"}, 'carriage-return=\\r\n'), ({"tab": "\t"}, 'tab=\\t\n'), ({"form-feed": "\f"}, 'form-feed=\\f\n'), ({"bell": "\a"}, 'bell=\\u0007\n'), ({"escape": "\x1B"}, 'escape=\\u001b\n'), ({"vertical-tab": "\v"}, 'vertical-tab=\\u000b\n'), ({"backslash": "\\"}, 'backslash=\\\\\n'), ({"equals": "="}, 'equals=\\=\n'), ({"colon": ":"}, 'colon=\\:\n'), ({"hash": "#"}, 'hash=\\#\n'), ({"exclamation": "!"}, 'exclamation=\\!\n'), ({"null": "\0"}, 'null=\\u0000\n'), ({"backspace": "\b"}, 'backspace=\\u0008\n'), ]) def test_dumps_no_ensure_ascii(d,s): assert dumps(d, timestamp=False, ensure_ascii=False) == s @pytest.mark.parametrize('c', [ '' 'foobar', ' leading', 'trailing ', ' ', 'This is a comment.', '#This is a double comment.', 'trailing newline\n', 'trailing CRLF\r\n', 'trailing carriage return\r', 'line one\nline two', 'line one\n#line two', 'line one\n!line two', '\0', '\a', '\b', '\t', '\n', '\v', '\f', '\r', '\x1B', '\x1F', '!', '#', ':', '=', '\\', '\\u2603', '~', '\x7F', '\x80', '\xA0', '\xF0', '\xFF', '\u0100', '\u2603', '\U0001F410', '\uDC10\uD83D', ''.join(unichr(i) for i in list(range(0x20)) + list(range(0x7F, 0xA0)) if i not in (10, 13)), ]) @pytest.mark.parametrize('ensure_ascii_comments', [None, True, False]) def test_dumps_comments(c, ensure_ascii_comments): s = dumps( {"key": "value"}, timestamp = False, comments = c, ensure_ascii_comments = ensure_ascii_comments, ) assert s == to_comment(c, ensure_ascii=ensure_ascii_comments) \ + '\nkey=value\n' if ensure_ascii_comments is None: assert s == dumps({"key": "value"}, timestamp=False, comments=c) @pytest.mark.parametrize('pout,ea', [ ("x\\u00f0=\\u2603\\ud83d\\udc10\n", True), ("x\xF0=\u2603\U0001F410\n", False), ]) @pytest.mark.parametrize('cout,eac', [ ("#x\\u00f0\\u2603\\ud83d\\udc10\n", True), ("#x\xF0\\u2603\\ud83d\\udc10\n", None), ("#x\xF0\u2603\U0001F410\n", False), ]) def test_dumps_ensure_ascii_cross_ensure_ascii_comments(pout, ea, cout, eac): assert dumps( {"x\xF0": "\u2603\U0001F410"}, timestamp = False, comments = "x\xF0\u2603\U0001F410", ensure_ascii = ea, ensure_ascii_comments = eac, ) == cout + pout def test_dumps_tab_separator(): assert dumps({"key": "value"}, separator='\t', timestamp=False) \ == 'key\tvalue\n' def test_dumps_timestamp_and_comment(): assert dumps( {"key": "value"}, comments='This is a comment.', timestamp=1473703254, ) == '#This is a comment.\n#Mon Sep 12 14:00:54 EDT 2016\nkey=value\n' javaproperties-0.6.0/test/test_dumps_xml.py000066400000000000000000000074551362623413500211730ustar00rootroot00000000000000from __future__ import unicode_literals from collections import OrderedDict import pytest from javaproperties import dumps_xml @pytest.mark.parametrize('d,s', [ ( {}, '\n' '\n' '\n' ), ( {"key": "value"}, '\n' '\n' 'value\n' '\n' ), ( [("key", "value"), ("zebra", "apple")], '\n' '\n' 'value\n' 'apple\n' '\n' ), ( [("zebra", "apple"), ("key", "value")], '\n' '\n' 'apple\n' 'value\n' '\n' ), ( OrderedDict([("key","value"), ("zebra","apple")]), '\n' '\n' 'value\n' 'apple\n' '\n' ), ( OrderedDict([("zebra","apple"), ("key","value")]), '\n' '\n' 'apple\n' 'value\n' '\n' ), ]) def test_dumps_xml(d,s): assert dumps_xml(d) == s @pytest.mark.parametrize('d,s', [ ( {"key": "value", "zebra": "apple"}, '\n' '\n' 'value\n' 'apple\n' '\n' ), ( [("key", "value"), ("zebra", "apple")], '\n' '\n' 'value\n' 'apple\n' '\n' ), ( [("zebra", "apple"), ("key", "value")], '\n' '\n' 'value\n' 'apple\n' '\n' ), ( OrderedDict([("key", "value"), ("zebra", "apple")]), '\n' '\n' 'value\n' 'apple\n' '\n' ), ( OrderedDict([("zebra", "apple"), ("key", "value")]), '\n' '\n' 'value\n' 'apple\n' '\n' ), ]) def test_dumps_xml_sorted(d,s): assert dumps_xml(d, sort_keys=True) == s def test_dumps_xml_comment(): assert dumps_xml({"key": "value"}, comment='This is a comment.') == '''\ This is a comment. value ''' def test_dumps_xml_entities(): assert dumps_xml({'&<>"\'': '&<>"\''}, comment='&<>"\'') == '''\ &<>"' &<>"' ''' javaproperties-0.6.0/test/test_escape.py000066400000000000000000000033041362623413500204100ustar00rootroot00000000000000from __future__ import unicode_literals import pytest from javaproperties import escape @pytest.mark.parametrize('sin,sout', [ ('', ''), ('foobar', 'foobar'), ('inner space', 'inner\\ space'), (' leading', '\\ leading'), ('trailing ', 'trailing\\ '), (' ', '\\ \\ \\ '), ('\0', '\\u0000'), ('\a', '\\u0007'), ('\b', '\\u0008'), ('\t', '\\t'), ('\n', '\\n'), ('\v', '\\u000b'), ('\f', '\\f'), ('\r', '\\r'), ('\x1B', '\\u001b'), ('\x1F', '\\u001f'), ('!', '\\!'), ('#', '\\#'), (':', '\\:'), ('=', '\\='), ('\\', '\\\\'), ('\\u2603', '\\\\u2603'), ('~', '~'), ('\x7F', '\\u007f'), ('\xF0', '\\u00f0'), ('\u2603', '\\u2603'), ('\U0001F410', '\\ud83d\\udc10'), ('\uDC10\uD83D', '\\udc10\\ud83d'), ]) def test_escape(sin, sout): assert escape(sin) == sout @pytest.mark.parametrize('sin,sout', [ ('', ''), ('foobar', 'foobar'), ('inner space', 'inner\\ space'), (' leading', '\\ leading'), ('trailing ', 'trailing\\ '), (' ', '\\ \\ \\ '), ('\0', '\\u0000'), ('\a', '\\u0007'), ('\b', '\\u0008'), ('\t', '\\t'), ('\n', '\\n'), ('\v', '\\u000b'), ('\f', '\\f'), ('\r', '\\r'), ('\x1B', '\\u001b'), ('\x1F', '\\u001f'), ('!', '\\!'), ('#', '\\#'), (':', '\\:'), ('=', '\\='), ('\\', '\\\\'), ('\\u2603', '\\\\u2603'), ('~', '~'), ('\x7F', '\\u007f'), ('\x80', '\x80'), ('\xA0', '\xA0'), ('\xF0', '\xF0'), ('\u2603', '\u2603'), ('\U0001F410', '\U0001F410'), ('\uDC10\uD83D', '\udc10\ud83d'), ]) def test_escape_no_ensure_ascii(sin, sout): assert escape(sin, ensure_ascii=False) == sout javaproperties-0.6.0/test/test_java_timestamp.py000066400000000000000000000123701362623413500221570ustar00rootroot00000000000000from __future__ import unicode_literals from datetime import datetime import platform import sys import time from dateutil.tz import tzstr import pytest from javaproperties import java_timestamp # Unix timestamps and datetime objects don't support leap seconds or month 13, # so there's no need (and no way) to test handling of them here. old_pacific = tzstr('PST8PDT,M4.1.0,M10.5.0') @pytest.mark.parametrize('ts,s', [ (None, ''), (False, ''), (0, 'Wed Dec 31 19:00:00 EST 1969'), (1234567890.101112, 'Fri Feb 13 18:31:30 EST 2009'), (1234567890.987654, 'Fri Feb 13 18:31:30 EST 2009'), # Months: (1451624400, 'Fri Jan 01 00:00:00 EST 2016'), (1454396522, 'Tue Feb 02 02:02:02 EST 2016'), (1456992183, 'Thu Mar 03 03:03:03 EST 2016'), (1459757044, 'Mon Apr 04 04:04:04 EDT 2016'), (1462439105, 'Thu May 05 05:05:05 EDT 2016'), (1465207566, 'Mon Jun 06 06:06:06 EDT 2016'), (1467889627, 'Thu Jul 07 07:07:07 EDT 2016'), (1470658088, 'Mon Aug 08 08:08:08 EDT 2016'), (1473426549, 'Fri Sep 09 09:09:09 EDT 2016'), (1476108610, 'Mon Oct 10 10:10:10 EDT 2016'), (1478880671, 'Fri Nov 11 11:11:11 EST 2016'), (1481562732, 'Mon Dec 12 12:12:12 EST 2016'), # Days of the week: (1451818800, 'Sun Jan 03 06:00:00 EST 2016'), (1451883600, 'Mon Jan 04 00:00:00 EST 2016'), (1451973600, 'Tue Jan 05 01:00:00 EST 2016'), (1452063600, 'Wed Jan 06 02:00:00 EST 2016'), (1452153600, 'Thu Jan 07 03:00:00 EST 2016'), (1452243600, 'Fri Jan 08 04:00:00 EST 2016'), (1452333600, 'Sat Jan 09 05:00:00 EST 2016'), # Leap day: (1456733655, 'Mon Feb 29 03:14:15 EST 2016'), # PM/24-hour time: (1463159593, 'Fri May 13 13:13:13 EDT 2016'), # Before spring ahead: (1457852399, 'Sun Mar 13 01:59:59 EST 2016'), (datetime(2016, 3, 13, 1, 59, 59), 'Sun Mar 13 01:59:59 EST 2016'), ( datetime(2006, 4, 2, 1, 59, 59, 0, old_pacific), 'Sun Apr 02 01:59:59 PST 2006', ), # Skipped by spring ahead: (datetime(2016, 3, 13, 2, 30, 0), 'Sun Mar 13 03:30:00 EDT 2016'), ( datetime(2006, 4, 2, 2, 30, 0, 0, old_pacific), 'Sun Apr 02 02:30:00 PDT 2006', ), # After spring ahead: (1457852401, 'Sun Mar 13 03:00:01 EDT 2016'), (datetime(2016, 3, 13, 3, 0, 1), 'Sun Mar 13 03:00:01 EDT 2016'), ( datetime(2006, 4, 2, 3, 0, 1, 0, old_pacific), 'Sun Apr 02 03:00:01 PDT 2006', ), # Before fall back: (1478411999, 'Sun Nov 06 01:59:59 EDT 2016'), (datetime(2016, 11, 6, 0, 59, 59), 'Sun Nov 06 00:59:59 EDT 2016'), ( datetime(2006, 10,29, 0, 59, 59, 0, old_pacific), 'Sun Oct 29 00:59:59 PDT 2006', ), # Duplicated by fall back: # Times duplicated by DST are interpreted non-deterministically by Python # pre-3.6 (cf. ), so there are two possible return # values for these calls. ( datetime(2016, 11, 6, 1, 30, 0), ('Sun Nov 06 01:30:00 EDT 2016', 'Sun Nov 06 01:30:00 EST 2016'), ), ( datetime(2006, 10,29, 1, 30, 0, 0, old_pacific), ('Sun Oct 29 01:30:00 PDT 2006', 'Sun Oct 29 01:30:00 PST 2006'), ), # After fall back: (1478412001, 'Sun Nov 06 01:00:01 EST 2016'), (datetime(2016, 11, 6, 2, 0, 1), 'Sun Nov 06 02:00:01 EST 2016'), ( datetime(2006, 10,29, 2, 0, 1, 0, old_pacific), 'Sun Oct 29 02:00:01 PST 2006', ), ]) def test_java_timestamp(ts, s): r = java_timestamp(ts) if isinstance(s, tuple): assert r in s else: assert r == s # Times duplicated by fall back, disambiguated with `fold`: @pytest.mark.skipif(sys.version_info[:2] < (3,6), reason='Python 3.6+ only') # Certain versions of pypy3.6 (including the one on Travis as of 2020-02-23) # have a bug in their datetime libraries that prevents the `fold` attribute # from working correctly. The latest known version to feature this bug is # 7.1.1 (Python version 3.6.1), and the earliest known version to feature a fix # is 7.2.0 (Python version 3.6.9); I don't *think* there were any releases in # between those two versions, but it isn't entirely clear. @pytest.mark.xfail( platform.python_implementation().lower() == 'pypy' and sys.version_info[:3] < (3,6,9), reason='Broken on this version of PyPy', ) @pytest.mark.parametrize('ts,fold,s', [ (datetime(2016, 11, 6, 1, 30, 0), 0, 'Sun Nov 06 01:30:00 EDT 2016'), ( datetime(2006, 10,29, 1, 30, 0, 0, old_pacific), 0, 'Sun Oct 29 01:30:00 PDT 2006', ), (datetime(2016, 11, 6, 1, 30, 0), 1, 'Sun Nov 06 01:30:00 EST 2016'), ( datetime(2006, 10,29, 1, 30, 0, 0, old_pacific), 1, 'Sun Oct 29 01:30:00 PST 2006', ), ]) def test_java_timestamp_fold(ts, fold, s): assert java_timestamp(ts.replace(fold=fold)) == s def test_java_timestamp_now(mocker): mocker.patch('time.localtime', return_value=time.localtime(1478550580)) assert java_timestamp() == 'Mon Nov 07 15:29:40 EST 2016' time.localtime.assert_called_once_with(None) def test_java_timestamp_dogfood_type_error(): with pytest.raises(TypeError) as excinfo: java_timestamp('Mon Dec 12 12:12:12 EST 2016') assert str(excinfo.value) == 'Timestamp must be number or datetime.datetime' javaproperties-0.6.0/test/test_join_key_value.py000066400000000000000000000065011362623413500221550ustar00rootroot00000000000000from __future__ import unicode_literals import pytest from javaproperties import join_key_value @pytest.mark.parametrize('key,value,s', [ ('', '', '='), ('key', 'value', 'key=value'), ('two words', 'value', 'two\\ words=value'), ('key', 'two words', 'key=two words'), (' key', 'value', '\\ key=value'), ('key', ' value', 'key=\\ value'), ('key ', 'value', 'key\\ =value'), ('key', 'value ', 'key=value '), (' ', 'value', '\\ \\ \\ =value'), ('key', ' ', 'key=\\ '), ('US', '\x1F', 'US=\\u001f'), ('tilde', '~', 'tilde=~'), ('delete', '\x7F', 'delete=\\u007f'), ('padding', '\x80', 'padding=\\u0080'), ('nbsp', '\xA0', 'nbsp=\\u00a0'), ('edh', '\xF0', 'edh=\\u00f0'), ('snowman', '\u2603', 'snowman=\\u2603'), ('goat', '\U0001F410', 'goat=\\ud83d\\udc10'), ('taog', '\uDC10\uD83D', 'taog=\\udc10\\ud83d'), ('newline', '\n', 'newline=\\n'), ('carriage-return', '\r', 'carriage-return=\\r'), ('tab', '\t', 'tab=\\t'), ('form-feed', '\f', 'form-feed=\\f'), ('bell', '\a', 'bell=\\u0007'), ('escape', '\x1B', 'escape=\\u001b'), ('vertical-tab', '\v', 'vertical-tab=\\u000b'), ('backslash', '\\', 'backslash=\\\\'), ('equals', '=', 'equals=\\='), ('colon', ':', 'colon=\\:'), ('hash', '#', 'hash=\\#'), ('exclamation', '!', 'exclamation=\\!'), ('null', '\0', 'null=\\u0000'), ('backspace', '\b', 'backspace=\\u0008'), ]) def test_join_key_value(key, value, s): assert join_key_value(key, value) == s @pytest.mark.parametrize('key,value,sep,s', [ ('key', 'value', ' = ', 'key = value'), ('key', 'value', ':', 'key:value'), ('key', 'value', ' ', 'key value'), ('key', 'value', '\t', 'key\tvalue'), (' key ', ' value ', ' : ', '\\ key\\ : \\ value '), ]) def test_join_key_value_separator(key, value, sep, s): assert join_key_value(key, value, separator=sep) == s @pytest.mark.parametrize('key,value,s', [ ('', '', '='), ('key', 'value', 'key=value'), ('two words', 'value', 'two\\ words=value'), ('key', 'two words', 'key=two words'), (' key', 'value', '\\ key=value'), ('key', ' value', 'key=\\ value'), ('key ', 'value', 'key\\ =value'), ('key', 'value ', 'key=value '), (' ', 'value', '\\ \\ \\ =value'), ('key', ' ', 'key=\\ '), ('US', '\x1F', 'US=\\u001f'), ('tilde', '~', 'tilde=~'), ('delete', '\x7F', 'delete=\\u007f'), ('padding', '\x80', 'padding=\x80'), ('nbsp', '\xA0', 'nbsp=\xA0'), ('edh', '\xF0', 'edh=\xF0'), ('snowman', '\u2603', 'snowman=\u2603'), ('goat', '\U0001F410', 'goat=\U0001F410'), ('taog', '\uDC10\uD83D', 'taog=\udc10\ud83d'), ('newline', '\n', 'newline=\\n'), ('carriage-return', '\r', 'carriage-return=\\r'), ('tab', '\t', 'tab=\\t'), ('form-feed', '\f', 'form-feed=\\f'), ('bell', '\a', 'bell=\\u0007'), ('escape', '\x1B', 'escape=\\u001b'), ('vertical-tab', '\v', 'vertical-tab=\\u000b'), ('backslash', '\\', 'backslash=\\\\'), ('equals', '=', 'equals=\\='), ('colon', ':', 'colon=\\:'), ('hash', '#', 'hash=\\#'), ('exclamation', '!', 'exclamation=\\!'), ('null', '\0', 'null=\\u0000'), ('backspace', '\b', 'backspace=\\u0008'), ]) def test_join_key_value_no_ensure_ascii(key, value, s): assert join_key_value(key, value, ensure_ascii=False) == s javaproperties-0.6.0/test/test_jpreplace.py000066400000000000000000000073501362623413500211220ustar00rootroot00000000000000from __future__ import unicode_literals import platform import sys import pytest import javaproperties # noqa @pytest.mark.parametrize('s,enc,b', [ ('foobar', 'us-ascii', b'foobar'), ('f\xFCbar', 'us-ascii', b'f\\u00fcbar'), ('f\xFC\xDFar', 'us-ascii', b'f\\u00fc\\u00dfar'), ('killer \u2603', 'us-ascii', b'killer \\u2603'), ('kid \U0001F410', 'us-ascii', b'kid \\ud83d\\udc10'), ('foobar', 'iso-8859-1', b'foobar'), ('f\xFCbar', 'iso-8859-1', b'f\xFCbar'), ('f\xFC\xDFar', 'iso-8859-1', b'f\xFC\xDFar'), ('killer \u2603', 'iso-8859-1', b'killer \\u2603'), ('kid \U0001F410', 'iso-8859-1', b'kid \\ud83d\\udc10'), ('foobar', 'utf-8', b'foobar'), ('f\xFCbar', 'utf-8', b'f\xC3\xBCbar'), ('f\xFC\xDFar', 'utf-8', b'f\xC3\xBC\xC3\x9Far'), ('killer \u2603', 'utf-8', b'killer \xE2\x98\x83'), ('kid \U0001F410', 'utf-8', b'kid \xF0\x9F\x90\x90'), ('foobar', 'utf-16be', 'foobar'.encode('utf-16be')), ('f\xFCbar', 'utf-16be', 'f\xFCbar'.encode('utf-16be')), ('f\xFC\xDFar', 'utf-16be', 'f\xFC\xDFar'.encode('utf-16be')), ('killer \u2603', 'utf-16be', 'killer \u2603'.encode('utf-16be')), ('kid \U0001F410', 'utf-16be', 'kid \U0001F410'.encode('utf-16be')), ('foobar', 'mac_roman', b'foobar'), ('f\xFCbar', 'mac_roman', b'f\x9Fbar'), ('f\xFC\xDFar', 'mac_roman', b'f\x9F\xA7ar'), ('killer \u2603', 'mac_roman', b'killer \\u2603'), ('kid \U0001F410', 'mac_roman', b'kid \\ud83d\\udc10'), ('e\xF0', 'mac_roman', b'e\\u00f0'), ('\u201CHello!\u201D', 'mac_roman', b'\xD2Hello!\xD3'), ('foobar', 'cp500', 'foobar'.encode('cp500')), ('f\xFCbar', 'cp500', 'f\xFCbar'.encode('cp500')), ('f\xFC\xDFar', 'cp500', 'f\xFC\xDFar'.encode('cp500')), ('killer \u2603', 'cp500', 'killer \\u2603'.encode('cp500')), ('kid \U0001F410', 'cp500', 'kid \\ud83d\\udc10'.encode('cp500')), ]) def test_javapropertiesreplace(s, enc, b): assert s.encode(enc, 'javapropertiesreplace') == b @pytest.mark.parametrize('s,esc', [ ('\uD83D\uDC10', '\\ud83d\\udc10'), ('\uD83D+\uDC10', '\\ud83d+\\udc10'), ('\uDC10\uD83D', '\\udc10\\ud83d'), ]) @pytest.mark.parametrize('enc', [ 'us-ascii', 'iso-8859-1', pytest.param( 'utf-8', marks=pytest.mark.skipif( sys.version_info[0] == 2, reason='Python 3 only', ), ), pytest.param( 'utf-16be', marks=[ pytest.mark.skipif( sys.version_info[0] == 2, reason='Python 3 only', ), # Certain versions of pypy3.6 (including the one on Travis as of # 2020-02-23) have a bug in their handling of encoding errors when # the target encoding is UTF-16. The latest known version to # feature this bug is 7.1.1 (Python version 3.6.1), and the # earliest known version after this to feature a fix is 7.2.0 # (Python version 3.6.9); I don't *think* there were any releases # in between those two versions, but it isn't entirely clear. pytest.mark.xfail( platform.python_implementation().lower() == 'pypy' and sys.version_info[:3] < (3,6,9), reason='Broken on this version of PyPy', ) ], ), 'mac_roman', 'cp500', ]) def test_javaproperties_bad_surrogates(s, enc, esc): assert s.encode(enc, 'javapropertiesreplace') == esc.encode(enc) javaproperties-0.6.0/test/test_load_xml.py000066400000000000000000000032621362623413500207520ustar00rootroot00000000000000from __future__ import unicode_literals import pytest from six import BytesIO from javaproperties import load_xml # The only thing special about `load_xml` compared to `loads_xml` is encoding, # so that's the only thing we'll test here. @pytest.mark.parametrize('b', [ b'''\ value ð 🐐 ''', b'''\ value \xF0 🐐 ''', '''\ value \xF0 \u2603 \U0001F410 '''.encode('utf-16be'), b'''\ value \xC3\xB0 \xE2\x98\x83 \xF0\x9F\x90\x90 ''', ]) def test_load_xml(b): assert load_xml(BytesIO(b)) == { 'key': 'value', 'edh': '\xF0', 'snowman': '\u2603', 'goat': '\U0001F410', } javaproperties-0.6.0/test/test_loads.py000066400000000000000000000153671362623413500202660ustar00rootroot00000000000000from __future__ import unicode_literals from collections import OrderedDict import pytest from javaproperties import InvalidUEscapeError, loads @pytest.mark.parametrize('s,d', [ ('key=value', {"key": "value"}), ("key", {"key": ""}), ("key ", {"key": ""}), ("key =value", {"key": "value"}), ("key= value", {"key": "value"}), ("key = value", {"key": "value"}), ("=value", {"": "value"}), (" =value", {"": "value"}), ("key=value ", {"key": "value "}), (" key=value", {"key": "value"}), (' = ', {"": ""}), ('=', {"": ""}), ('', {}), (' ', {}), ('\n', {}), ('\r\n', {}), ('\r', {}), ('#This is a comment.', {}), ('#This is a comment.\nkey = value', {"key": "value"}), ('key = value\n#This is a comment.', {"key": "value"}), ('!This is a comment.', {}), ('!This is a comment.\nkey = value', {"key": "value"}), ('key = value\n!This is a comment.', {"key": "value"}), ('key = val\\\nue', {"key": "value"}), ('key = val\\\n ue', {"key": "value"}), ('key = val \\\nue', {"key": "val ue"}), ('key = val \\\n ue', {"key": "val ue"}), ('ke\\\ny = value', {"key": "value"}), ('ke\\\n y = value', {"key": "value"}), ('one two three', {"one": "two three"}), ('key=value\n', {"key": "value"}), ('key=value\r\n', {"key": "value"}), ('key=value\r', {"key": "value"}), ('key:value', {"key": "value"}), ('key value', {"key": "value"}), ('\\ key\\ = \\ value ', {" key ": " value "}), ('\\ key\\ : \\ value ', {" key ": " value "}), ('\\ key\\ \t \\ value ', {" key ": " value "}), ('\\ key\\ \\ value ', {" key ": " value "}), ('\\ key\\ =\\ value ', {" key ": " value "}), ('\\ key\\ :\\ value ', {" key ": " value "}), ('\\ key\\ \\ value ', {" key ": " value "}), ('\\ key\\ \t\\ value ', {" key ": " value "}), ('goat = \\uD83D\\uDC10', {"goat": "\U0001F410"}), ('taog = \\uDC10\\uD83D', {"taog": "\uDC10\uD83D"}), ('goat = \uD83D\uDC10', {"goat": "\U0001F410"}), ('goat = \uD83D\\uDC10', {"goat": "\U0001F410"}), ('goat = \\uD83D\uDC10', {"goat": "\U0001F410"}), ('taog = \uDC10\uD83D', {"taog": "\uDC10\uD83D"}), ('goat = \\uD83D\\\n \\uDC10', {"goat": "\U0001F410"}), ('\\\n# comment', {"#": "comment"}), (' \\\n# comment', {"#": "comment"}), ('key = value\\\n # comment', {"key": "value# comment"}), ('key = value\\\n', {"key": "value"}), ('key = value\\', {"key": "value"}), ('key = value\\\n ', {"key": "value"}), ('# comment\\\nkey = value', {"key": "value"}), ('\\\n', {}), ('\\\nkey = value', {"key": "value"}), (' \\\nkey = value', {"key": "value"}), ('key = value\nfoo = bar', {"key": "value", "foo": "bar"}), ('key = value\r\nfoo = bar', {"key": "value", "foo": "bar"}), ('key = value\rfoo = bar', {"key": "value", "foo": "bar"}), ('key = value1\nkey = value2', {"key": "value2"}), ('snowman = \\u2603', {"snowman": "\u2603"}), ('pokmon = \\u00E9', {"pokmon": "\u00E9"}), ('newline = \\u000a', {"newline": "\n"}), ('key = value\\\n\\\nend', {"key": "valueend"}), ('key = value\\\n \\\nend', {"key": "valueend"}), ('key = value\\\\\nend', {"key": "value\\", "end": ""}), ('c#sharp = sucks', {"c#sharp": "sucks"}), ('fifth = #5', {"fifth": "#5"}), ('edh = \xF0', {"edh": "\xF0"}), ('snowman = \u2603', {"snowman": "\u2603"}), ('goat = \U0001F410', {"goat": "\U0001F410"}), ('newline = \\n', {"newline": "\n"}), ('tab = \\t', {"tab": "\t"}), ('form.feed = \\f', {"form.feed": "\f"}), ('two\\ words = one key', {"two words": "one key"}), ('hour\\:minute = 1440', {"hour:minute": "1440"}), ('E\\=mc^2 = Einstein', {"E=mc^2": "Einstein"}), ('two\\\\ words = not a key', {"two\\": "words = not a key"}), ('two\\\\\\ words = one key', {"two\\ words": "one key"}), ('invalid-escape = \\0', {"invalid-escape": "0"}), ('invalid-escape = \\q', {"invalid-escape": "q"}), ('invalid-escape = \\?', {"invalid-escape": "?"}), ('invalid-escape = \\x40', {"invalid-escape": "x40"}), (' \\ key = value', {" key": "value"}), (' \\u0020key = value', {" key": "value"}), (' \\ key = value', {" ": "key = value"}), ('key = \\ value', {"key": " value"}), ('\nkey = value', {"key": "value"}), (' \nkey = value', {"key": "value"}), ('key = value\n', {"key": "value"}), ('key = value\n ', {"key": "value"}), ('key = value\n\nfoo = bar', {"key": "value", "foo": "bar"}), ('key = value\n \nfoo = bar', {"key": "value", "foo": "bar"}), (b'key=value\nedh=\xF0', {"key": "value", "edh": "\xF0"}), ( b'key=value\n' b'edh=\xC3\xB0\n' b'snowman=\xE2\x98\x83\n' b'goat=\xF0\x9F\x90\x90', { 'key': 'value', 'edh': '\xC3\xB0', 'snowman': '\xE2\x98\x83', 'goat': '\xF0\x9F\x90\x90', }, ), ('key\tvalue=pair', {"key": "value=pair"}), ('key\\\tvalue=pair', {"key\tvalue": "pair"}), ('key\fvalue=pair', {"key": "value=pair"}), ('key\\\fvalue=pair', {"key\fvalue": "pair"}), ('key\0value', {"key\0value": ""}), ('key\\\0value', {"key\0value": ""}), ('the = \\u00f0e', {"the": "\xF0e"}), ('\\u00f0e = the', {"\xF0e": "the"}), ('goat = \\U0001F410', {"goat": "U0001F410"}), ('key\\u003Dvalue', {"key=value": ""}), ('key\\u003Avalue', {"key:value": ""}), ('key\\u0020value', {"key value": ""}), ('key=\\\\u2603', {"key": "\\u2603"}), ('key=\\\\u260x', {"key": "\\u260x"}), ]) def test_loads(s,d): assert loads(s) == d def test_loads_multiple_ordereddict(): assert loads('key = value\nfoo = bar', object_pairs_hook=OrderedDict) == \ OrderedDict([("key", "value"), ("foo", "bar")]) def test_loads_multiple_ordereddict_rev(): assert loads('foo = bar\nkey = value', object_pairs_hook=OrderedDict) == \ OrderedDict([("foo", "bar"), ("key", "value")]) @pytest.mark.parametrize('s,esc', [ ('\\u = bad', '\\u'), ('\\u abcx = bad', '\\u'), ('\\u', '\\u'), ('\\uab bad', '\\uab'), ('\\uab:bad', '\\uab'), ('\\uab=bad', '\\uab'), ('\\uabc = bad', '\\uabc'), ('\\uabcx = bad', '\\uabcx'), ('\\ux = bad', '\\ux'), ('\\uxabc = bad', '\\uxabc'), ('bad = \\u ', '\\u '), ('bad = \\u abcx', '\\u abc'), ('bad = \\u', '\\u'), ('bad = \\uab\\cd', '\\uab\\c'), ('bad = \\uab\\u0063d', '\\uab\\u'), ('bad = \\uabc ', '\\uabc '), ('bad = \\uabc', '\\uabc'), ('bad = \\uabcx', '\\uabcx'), ('bad = \\ux', '\\ux'), ('bad = \\uxabc', '\\uxabc'), ]) def test_loads_invalid_u_escape(s, esc): with pytest.raises(InvalidUEscapeError) as excinfo: loads(s) assert excinfo.value.escape == esc assert str(excinfo.value) == 'Invalid \\u escape sequence: ' + esc javaproperties-0.6.0/test/test_loads_xml.py000066400000000000000000000065151362623413500211410ustar00rootroot00000000000000from __future__ import unicode_literals from collections import OrderedDict import pytest from javaproperties import loads_xml @pytest.mark.parametrize('s,d', [ ('', {}), ( 'value', {"key": "value"}, ), ( ' ', {"key": " "}, ), ( '\n', {"key": "\n"}, ), ( '' '\n' 'bar' '', {"key": "\n"}, ), ( '\n value\n\n', {"key": "value"}, ), ( '' 'value' 'bar' '', {"key": "value", "foo": "bar"}, ), ( '\n' ' value1\n' ' value2\n' '\n', {"key": "value2"} ), ( '\n' ' &\n' ' <\n' ' >\n' ' "\n' ' \n' '\n', { "ampersand": "&", "less than": "<", "greater than": ">", '"': '"', "snowman": "\u2603", }, ), ( '\n' ' \\n\\r\\t\\u2603\\f\\\\\n' '\n', {"escapes": "\\n\\r\\t\\u2603\\f\\\\"}, ), ( '\n' ' This is a comment.\n' ' value\n' '\n', {"key": "value"}, ), ( '\n' ' value\n' ' bar\n' '\n', {"key": "value"}, ), ( '🐐', {"goat": "\U0001F410"}, ), ]) def test_loads_xml(s,d): assert loads_xml(s) == d def test_loads_xml_bad_root(): with pytest.raises(ValueError) as excinfo: loads_xml('value') assert 'not rooted at ' in str(excinfo.value) def test_loads_xml_no_key(): with pytest.raises(ValueError) as excinfo: loads_xml('value') assert ' is missing "key" attribute' in str(excinfo.value) def test_loads_xml_multiple_ordereddict(): assert loads_xml(''' value bar ''', object_pairs_hook=OrderedDict) == \ OrderedDict([("key", "value"), ("foo", "bar")]) def test_loads_xml_multiple_ordereddict_rev(): assert loads_xml(''' bar value ''', object_pairs_hook=OrderedDict) == \ OrderedDict([("foo", "bar"), ("key", "value")]) javaproperties-0.6.0/test/test_propclass.py000066400000000000000000000460071362623413500211650ustar00rootroot00000000000000from __future__ import unicode_literals import time import pytest from six import PY2, BytesIO, StringIO from javaproperties import Properties, dumps if PY2: from collections import Iterator else: from collections.abc import Iterator # Making the global INPUT object a StringIO would cause it be exhausted after # the first test and thereafter appear to be empty. Thus, a new StringIO must # be created for each test instead. INPUT = '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo: first definition bar=only definition # Comment between values key = value zebra \\ apple foo : second definition # Comment at end of file ''' XML_INPUT = '''\ Thu Mar 16 17:06:52 EDT 2017 first definition only definition value apple second definition ''' def test_propclass_empty(mocker): mocker.patch('time.localtime', return_value=time.localtime(1478550580)) p = Properties() assert len(p) == 0 assert not bool(p) assert dict(p) == {} s = StringIO() p.store(s) assert s.getvalue() == '#Mon Nov 07 15:29:40 EST 2016\n' time.localtime.assert_called_once_with(None) def test_propclass_load(): p = Properties() p.load(StringIO(INPUT)) assert len(p) == 4 assert bool(p) assert dict(p) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } def test_propclass_nonempty_load(): p = Properties({"key": "lock", "horse": "orange"}) p.load(StringIO(INPUT)) assert len(p) == 5 assert bool(p) assert dict(p) == { "foo": "second definition", "bar": "only definition", "horse": "orange", "key": "value", "zebra": "apple", } def test_propclass_loadFromXML(): p = Properties() p.loadFromXML(StringIO(XML_INPUT)) assert len(p) == 4 assert bool(p) assert dict(p) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } def test_propclass_nonempty_loadFromXML(): p = Properties({"key": "lock", "horse": "orange"}) p.loadFromXML(StringIO(XML_INPUT)) assert len(p) == 5 assert bool(p) assert dict(p) == { "foo": "second definition", "bar": "only definition", "horse": "orange", "key": "value", "zebra": "apple", } def test_propclass_getitem(): p = Properties() p.load(StringIO(INPUT)) assert p["key"] == "value" assert p["foo"] == "second definition" with pytest.raises(KeyError): p["missing"] def test_propclass_setitem(): p = Properties() p.load(StringIO(INPUT)) p["key"] = "lock" assert len(p) == 4 assert bool(p) assert dict(p) == { "foo": "second definition", "bar": "only definition", "key": "lock", "zebra": "apple", } def test_propclass_additem(): p = Properties() p.load(StringIO(INPUT)) p["new"] = "old" assert len(p) == 5 assert bool(p) assert dict(p) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", "new": "old", } def test_propclass_delitem(): p = Properties() p.load(StringIO(INPUT)) del p["key"] assert len(p) == 3 assert bool(p) assert dict(p) == { "foo": "second definition", "bar": "only definition", "zebra": "apple", } def test_propclass_delitem_missing(): p = Properties() p.load(StringIO(INPUT)) with pytest.raises(KeyError): del p["missing"] assert len(p) == 4 assert bool(p) assert dict(p) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } def test_propclass_from_dict(): p = Properties({"key": "value", "apple": "zebra"}) assert len(p) == 2 assert bool(p) assert dict(p) == {"apple": "zebra", "key": "value"} def test_propclass_from_pairs_list(): p = Properties([("key", "value"), ("apple", "zebra")]) assert len(p) == 2 assert bool(p) assert dict(p) == {"apple": "zebra", "key": "value"} def test_propclass_copy(): p = Properties({"Foo": "bar"}) p2 = p.copy() assert p is not p2 assert isinstance(p2, Properties) assert p == p2 assert dict(p) == dict(p2) == {"Foo": "bar"} p2["Foo"] = "gnusto" assert dict(p) == {"Foo": "bar"} assert dict(p2) == {"Foo": "gnusto"} assert p != p2 p2["fOO"] = "quux" assert dict(p) == {"Foo": "bar"} assert dict(p2) == {"Foo": "gnusto", "fOO": "quux"} assert p != p2 def test_propclass_copy_more(): p = Properties() p.load(StringIO(INPUT)) p2 = p.copy() assert p is not p2 assert isinstance(p2, Properties) assert p == p2 assert dict(p) == dict(p2) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } p2["foo"] = "third definition" del p2["bar"] p2["key"] = "value" p2["zebra"] = "horse" p2["new"] = "old" assert p != p2 assert dict(p) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } assert dict(p2) == { "foo": "third definition", "key": "value", "zebra": "horse", "new": "old", } def test_propclass_eq_empty(): p = Properties() p2 = Properties() assert p is not p2 assert p == p2 assert p2 == p def test_propclass_defaults_neq_empty(): p = Properties() p2 = Properties(defaults=Properties({"key": "lock", "horse": "orange"})) assert p != p2 assert p2 != p def test_propclass_eq_nonempty(): p = Properties({"Foo": "bar"}) p2 = Properties({"Foo": "bar"}) assert p is not p2 assert p == p2 assert p2 == p def test_propclass_eq_nonempty_defaults(): p = Properties({"Foo": "bar"}, defaults=Properties({"key": "lock"})) p2 = Properties({"Foo": "bar"}, defaults=Properties({"key": "lock"})) assert p is not p2 assert p == p2 assert p2 == p def test_propclass_neq_nonempty_neq_defaults(): p = Properties({"Foo": "bar"}, defaults=Properties({"key": "lock"})) p2 = Properties({"Foo": "bar"}, defaults=Properties({"key": "Florida"})) assert p != p2 assert p2 != p def test_propclass_eq_self(): p = Properties() p.load(StringIO(INPUT)) assert p == p def test_propclass_neq(): assert Properties({"Foo": "bar"}) != Properties({"Foo": "BAR"}) def test_propclass_eq_dict(): p = Properties({"Foo": "BAR"}) assert p == {"Foo": "BAR"} assert {"Foo": "BAR"} == p assert p != {"Foo": "bar"} assert {"Foo": "bar"} != p def test_propclass_defaults_eq_dict(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"Foo": "BAR"}, defaults=defs) assert p == {"Foo": "BAR"} assert {"Foo": "BAR"} == p assert p != {"Foo": "bar"} assert {"Foo": "bar"} != p def test_propclass_eq_set_nochange(): p = Properties() p.load(StringIO(INPUT)) p2 = Properties() p2.load(StringIO(INPUT)) assert p == p2 assert p["key"] == p2["key"] == "value" p2["key"] = "value" assert p == p2 assert dict(p) == dict(p2) def test_propclass_eq_one_comment(): p = Properties() p.load(StringIO('#This is a comment.\nkey=value\n')) p2 = Properties() p2.load(StringIO('key=value\n')) assert p == p2 assert dict(p) == dict(p2) def test_propclass_eq_different_comments(): p = Properties() p.load(StringIO('#This is a comment.\nkey=value\n')) p2 = Properties() p2.load(StringIO('#This is also a comment.\nkey=value\n')) assert p == p2 assert dict(p) == dict(p2) def test_propclass_eq_one_repeated_key(): p = Properties() p.load(StringIO('key = value\nkey: other value\n')) p2 = Properties() p2.load(StringIO('key other value')) assert p == p2 assert dict(p) == dict(p2) == {"key": "other value"} def test_propclass_eq_repeated_keys(): p = Properties() p.load(StringIO('key = value\nkey: other value\n')) p2 = Properties() p2.load(StringIO('key: whatever\nkey other value')) assert p == p2 assert dict(p) == dict(p2) == {"key": "other value"} def test_propclass_load_eq_from_dict(): p = Properties() p.load(StringIO(INPUT)) assert p == Properties({ "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", }) def test_propclass_neq_string(): p = Properties() p.load(StringIO(INPUT)) assert p != INPUT assert INPUT != p def test_propclass_propertyNames(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) names = p.propertyNames() assert isinstance(names, Iterator) assert sorted(names) == ["apple", "foo", "key"] def test_propclass_stringPropertyNames(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) assert p.stringPropertyNames() == set(["key", "apple", "foo"]) def test_propclass_getProperty(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) assert p.getProperty("key") == "value" def test_propclass_getProperty_default(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) assert p.getProperty("key", "default") == "value" def test_propclass_getProperty_missing(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) assert p.getProperty("missing") is None def test_propclass_getProperty_missing_default(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) assert p.getProperty("missing", "default") == "default" def test_propclass_get_nonstring_key(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) with pytest.raises(TypeError) as excinfo: p[42] assert str(excinfo.value) == \ 'Keys & values of Properties instances must be strings' def test_propclass_set_nonstring_key(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) with pytest.raises(TypeError) as excinfo: p[42] = 'forty-two' assert str(excinfo.value) == \ 'Keys & values of Properties instances must be strings' def test_propclass_set_nonstring_value(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) with pytest.raises(TypeError) as excinfo: p['forty-two'] = 42 assert str(excinfo.value) == \ 'Keys & values of Properties instances must be strings' def test_propclass_del_nonstring_key(): p = Properties({"key": "value", "apple": "zebra", "foo": "bar"}) with pytest.raises(TypeError) as excinfo: del p[42] assert str(excinfo.value) == \ 'Keys & values of Properties instances must be strings' def test_propclass_from_nonstring_key(): with pytest.raises(TypeError) as excinfo: Properties({"key": "value", 42: "forty-two"}) assert str(excinfo.value) == \ 'Keys & values of Properties instances must be strings' def test_propclass_from_nonstring_value(): with pytest.raises(TypeError) as excinfo: Properties({"key": "value", "forty-two": 42}) assert str(excinfo.value) == \ 'Keys & values of Properties instances must be strings' def test_propclass_defaults(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) assert len(p) == 2 assert bool(p) assert dict(p) == {"key": "value", "apple": "zebra"} def test_propclass_defaults_getitem(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) assert p["apple"] == "zebra" def test_propclass_defaults_getitem_overridden(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) assert p["key"] == "value" def test_propclass_defaults_getitem_defaulted(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) with pytest.raises(KeyError): p["horse"] def test_propclass_defaults_getProperty(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) assert p.getProperty("apple") == "zebra" def test_propclass_defaults_getProperty_overridden(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) assert p.getProperty("key") == "value" def test_propclass_defaults_getProperty_defaulted(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) assert p.getProperty("horse") == "orange" def test_propclass_defaults_propertyNames(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) names = p.propertyNames() assert isinstance(names, Iterator) assert sorted(names) == ["apple", "horse", "key"] def test_propclass_defaults_stringPropertyNames(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) assert p.stringPropertyNames() == set(["key", "apple", "horse"]) def test_propclass_setProperty(): p = Properties() p.load(StringIO(INPUT)) p.setProperty("key", "lock") assert len(p) == 4 assert bool(p) assert dict(p) == { "foo": "second definition", "bar": "only definition", "key": "lock", "zebra": "apple", } def test_propclass_defaults_setProperty(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) p.setProperty("apple", "banana") assert dict(p) == {"key": "value", "apple": "banana"} assert dict(defs) == {"key": "lock", "horse": "orange"} def test_propclass_defaults_setProperty_overridden(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) p.setProperty("key", "hole") assert dict(p) == {"key": "hole", "apple": "zebra"} assert dict(defs) == {"key": "lock", "horse": "orange"} def test_propclass_defaults_setProperty_new(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) p.setProperty("new", "old") assert dict(p) == {"key": "value", "apple": "zebra", "new": "old"} assert dict(defs) == {"key": "lock", "horse": "orange"} def test_propclass_defaults_setProperty_new_override(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) p.setProperty("horse", "pony") assert dict(p) == {"key": "value", "apple": "zebra", "horse": "pony"} assert dict(defs) == {"key": "lock", "horse": "orange"} def test_propclass_defaults_setitem(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) p["apple"] = "banana" assert dict(p) == {"key": "value", "apple": "banana"} assert dict(defs) == {"key": "lock", "horse": "orange"} def test_propclass_defaults_setitem_overridden(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) p["key"] = "hole" assert dict(p) == {"key": "hole", "apple": "zebra"} assert dict(defs) == {"key": "lock", "horse": "orange"} def test_propclass_defaults_setitem_new(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) p["new"] = "old" assert dict(p) == {"key": "value", "apple": "zebra", "new": "old"} assert dict(defs) == {"key": "lock", "horse": "orange"} def test_propclass_defaults_setitem_new_override(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value", "apple": "zebra"}, defaults=defs) p["horse"] = "pony" assert dict(p) == {"key": "value", "apple": "zebra", "horse": "pony"} assert dict(defs) == {"key": "lock", "horse": "orange"} def test_propclass_empty_setitem(mocker): mocker.patch('time.localtime', return_value=time.localtime(1478550580)) p = Properties() p["key"] = "value" assert len(p) == 1 assert bool(p) assert dict(p) == {"key": "value"} s = StringIO() p.store(s) assert s.getvalue() == '#Mon Nov 07 15:29:40 EST 2016\nkey=value\n' time.localtime.assert_called_once_with(None) def test_propclass_store(mocker): mocker.patch('time.localtime', return_value=time.localtime(1478550580)) p = Properties({"key": "value"}) s = StringIO() p.store(s) assert s.getvalue() == '#Mon Nov 07 15:29:40 EST 2016\nkey=value\n' time.localtime.assert_called_once_with(None) def test_propclass_store_comment(mocker): mocker.patch('time.localtime', return_value=time.localtime(1478550580)) p = Properties({"key": "value"}) s = StringIO() p.store(s, comments='Testing') assert s.getvalue() == \ '#Testing\n#Mon Nov 07 15:29:40 EST 2016\nkey=value\n' time.localtime.assert_called_once_with(None) def test_propclass_store_defaults(mocker): mocker.patch('time.localtime', return_value=time.localtime(1478550580)) defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value"}, defaults=defs) s = StringIO() p.store(s) assert s.getvalue() == '#Mon Nov 07 15:29:40 EST 2016\nkey=value\n' time.localtime.assert_called_once_with(None) def test_propclass_storeToXML(): p = Properties({"key": "value"}) s = BytesIO() p.storeToXML(s) assert s.getvalue() == b'''\ value ''' def test_propclass_storeToXML_comment(): p = Properties({"key": "value"}) s = BytesIO() p.storeToXML(s, comment='Testing') assert s.getvalue() == b'''\ Testing value ''' def test_propclass_storeToXML_defaults(): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value"}, defaults=defs) s = BytesIO() p.storeToXML(s) assert s.getvalue() == b'''\ value ''' def test_propclass_dumps_function(): assert dumps(Properties({"key": "value"}), timestamp=False) == 'key=value\n' # defaults with defaults javaproperties-0.6.0/test/test_propfile.py000066400000000000000000000421421362623413500207730ustar00rootroot00000000000000from __future__ import unicode_literals from collections import OrderedDict import pytest from javaproperties import PropertiesFile, dumps INPUT = '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo: first definition bar=only definition # Comment between values key = value zebra \\ apple foo : second definition # Comment at end of file ''' def test_propfile_empty(): pf = PropertiesFile() pf._check() assert len(pf) == 0 assert not bool(pf) assert dict(pf) == {} assert list(pf) == [] assert list(reversed(pf)) == [] assert pf.dumps() == '' def test_propfile_loads(): pf = PropertiesFile.loads(INPUT) pf._check() assert len(pf) == 4 assert bool(pf) assert dict(pf) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } assert list(pf) == ["foo", "bar", "key", "zebra"] assert list(reversed(pf)) == ["zebra", "key", "bar", "foo"] def test_propfile_dumps(): pf = PropertiesFile.loads(INPUT) pf._check() assert pf.dumps() == INPUT def test_propfile_getitem(): pf = PropertiesFile.loads(INPUT) pf._check() assert pf["key"] == "value" assert pf["foo"] == "second definition" with pytest.raises(KeyError): pf["missing"] pf._check() def test_propfile_setitem(): pf = PropertiesFile.loads(INPUT) pf._check() pf["key"] = "lock" pf._check() assert dict(pf) == { "foo": "second definition", "bar": "only definition", "key": "lock", "zebra": "apple", } assert list(pf) == ["foo", "bar", "key", "zebra"] assert list(reversed(pf)) == ["zebra", "key", "bar", "foo"] assert pf.dumps() == '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo: first definition bar=only definition # Comment between values key=lock zebra \\ apple foo : second definition # Comment at end of file ''' def test_propfile_additem(): pf = PropertiesFile.loads(INPUT) pf._check() pf["new"] = "old" pf._check() assert dict(pf) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", "new": "old", } assert list(pf) == ["foo", "bar", "key", "zebra", "new"] assert list(reversed(pf)) == ["new", "zebra", "key", "bar", "foo"] assert pf.dumps() == '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo: first definition bar=only definition # Comment between values key = value zebra \\ apple foo : second definition # Comment at end of file new=old ''' def test_propfile_delitem(): pf = PropertiesFile.loads(INPUT) pf._check() del pf["key"] pf._check() assert dict(pf) == { "foo": "second definition", "bar": "only definition", "zebra": "apple", } assert list(pf) == ["foo", "bar", "zebra"] assert list(reversed(pf)) == ["zebra", "bar", "foo"] assert pf.dumps() == '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo: first definition bar=only definition # Comment between values zebra \\ apple foo : second definition # Comment at end of file ''' def test_propfile_delitem_missing(): pf = PropertiesFile.loads(INPUT) pf._check() with pytest.raises(KeyError): del pf["missing"] pf._check() assert len(pf) == 4 assert bool(pf) assert dict(pf) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } assert list(pf) == ["foo", "bar", "key", "zebra"] assert list(reversed(pf)) == ["zebra", "key", "bar", "foo"] assert pf.dumps() == INPUT def test_propfile_move_item(): pf = PropertiesFile.loads(INPUT) pf._check() del pf["key"] pf._check() pf["key"] = "recreated" pf._check() assert dict(pf) == { "foo": "second definition", "bar": "only definition", "key": "recreated", "zebra": "apple", } assert list(pf) == ["foo", "bar", "zebra", "key"] assert list(reversed(pf)) == ["key", "zebra", "bar", "foo"] assert pf.dumps() == '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo: first definition bar=only definition # Comment between values zebra \\ apple foo : second definition # Comment at end of file key=recreated ''' def test_propfile_set_nochange(): pf = PropertiesFile.loads(INPUT) pf._check() assert pf["key"] == "value" pf["key"] = "value" pf._check() assert dict(pf) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } assert list(pf) == ["foo", "bar", "key", "zebra"] assert list(reversed(pf)) == ["zebra", "key", "bar", "foo"] assert pf.dumps() == '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo: first definition bar=only definition # Comment between values key=value zebra \\ apple foo : second definition # Comment at end of file ''' def test_propfile_dumps_function(): assert dumps(PropertiesFile.loads(INPUT), timestamp=False) == '''\ foo=second definition bar=only definition key=value zebra=apple ''' def test_propfile_set_repeated_key(): pf = PropertiesFile.loads(INPUT) pf._check() pf["foo"] = "redefinition" pf._check() assert dict(pf) == { "foo": "redefinition", "bar": "only definition", "key": "value", "zebra": "apple", } assert list(pf) == ["foo", "bar", "key", "zebra"] assert list(reversed(pf)) == ["zebra", "key", "bar", "foo"] assert pf.dumps() == '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo=redefinition bar=only definition # Comment between values key = value zebra \\ apple # Comment at end of file ''' def test_propfile_delete_repeated_key(): pf = PropertiesFile.loads(INPUT) pf._check() del pf["foo"] pf._check() assert dict(pf) == { "bar": "only definition", "key": "value", "zebra": "apple", } assert list(pf) == ["bar", "key", "zebra"] assert list(reversed(pf)) == ["zebra", "key", "bar"] assert pf.dumps() == '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp bar=only definition # Comment between values key = value zebra \\ apple # Comment at end of file ''' def test_propfile_from_ordereddict(): pf = PropertiesFile(OrderedDict([('key', 'value'), ('apple', 'zebra')])) pf._check() assert len(pf) == 2 assert bool(pf) assert dict(pf) == {"apple": "zebra", "key": "value"} assert list(pf) == ["key", "apple"] assert list(reversed(pf)) == ["apple", "key"] assert pf.dumps() == 'key=value\napple=zebra\n' def test_propfile_from_kwarg(): pf = PropertiesFile(key='value') pf._check() assert len(pf) == 1 assert bool(pf) assert dict(pf) == {"key": "value"} assert list(pf) == ["key"] assert list(reversed(pf)) == ["key"] assert pf.dumps() == 'key=value\n' def test_propfile_from_pairs_list(): pf = PropertiesFile([('key', 'value'), ('apple', 'zebra')]) pf._check() assert len(pf) == 2 assert bool(pf) assert dict(pf) == {"apple": "zebra", "key": "value"} assert list(pf) == ["key", "apple"] assert list(reversed(pf)) == ["apple", "key"] assert pf.dumps() == 'key=value\napple=zebra\n' def test_propfile_from_ordereddict_and_kwarg(): pf = PropertiesFile(OrderedDict([('key', 'value'), ('apple', 'zebra')]), key='lock') pf._check() assert len(pf) == 2 assert bool(pf) assert dict(pf) == {"apple": "zebra", "key": "lock"} assert list(pf) == ["key", "apple"] assert list(reversed(pf)) == ["apple", "key"] assert pf.dumps() == 'key=lock\napple=zebra\n' def test_propfile_dumps_separator(): pf = PropertiesFile.loads(INPUT) pf._check() assert pf.dumps(separator='\t') == INPUT def test_propfile_set_dumps_separator(): pf = PropertiesFile.loads(INPUT) pf._check() pf["key"] = "lock" pf._check() assert pf.dumps(separator='\t') == '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo: first definition bar=only definition # Comment between values key\tlock zebra \\ apple foo : second definition # Comment at end of file ''' def test_propfile_copy(): pf = PropertiesFile({"Foo": "bar"}) pf2 = pf.copy() pf._check() pf2._check() assert pf is not pf2 assert isinstance(pf2, PropertiesFile) assert pf == pf2 assert dict(pf) == dict(pf2) == {"Foo": "bar"} pf2["Foo"] = "gnusto" pf._check() pf2._check() assert dict(pf) == {"Foo": "bar"} assert dict(pf2) == {"Foo": "gnusto"} assert pf != pf2 pf2["fOO"] = "quux" pf._check() pf2._check() assert dict(pf) == {"Foo": "bar"} assert dict(pf2) == {"Foo": "gnusto", "fOO": "quux"} assert pf != pf2 def test_propfile_copy_more(): pf = PropertiesFile.loads(INPUT) pf2 = pf.copy() pf._check() pf2._check() assert pf is not pf2 assert isinstance(pf2, PropertiesFile) assert pf == pf2 assert dict(pf) == dict(pf2) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } pf2["foo"] = "third definition" del pf2["bar"] pf2["key"] = "value" pf2["zebra"] = "horse" pf2["new"] = "old" pf._check() pf2._check() assert pf != pf2 assert dict(pf) == { "foo": "second definition", "bar": "only definition", "key": "value", "zebra": "apple", } assert dict(pf2) == { "foo": "third definition", "key": "value", "zebra": "horse", "new": "old", } assert pf.dumps() == INPUT assert pf2.dumps() == '''\ # A comment before the timestamp #Thu Mar 16 17:06:52 EDT 2017 # A comment after the timestamp foo=third definition # Comment between values key=value zebra=horse # Comment at end of file new=old ''' def test_propfile_eq_empty(): pf = PropertiesFile() pf2 = PropertiesFile() assert pf is not pf2 assert pf == pf2 def test_propfile_eq_nonempty(): pf = PropertiesFile({"Foo": "bar"}) pf2 = PropertiesFile({"Foo": "bar"}) assert pf is not pf2 assert pf == pf2 def test_propfile_eq_self(): pf = PropertiesFile.loads(INPUT) assert pf == pf def test_propfile_neq(): assert PropertiesFile({"Foo": "bar"}) != PropertiesFile({"Foo": "BAR"}) def test_propfile_eq_dict(): pf = PropertiesFile({"Foo": "BAR"}) assert pf == {"Foo": "BAR"} assert {"Foo": "BAR"} == pf assert pf != {"Foo": "bar"} assert {"Foo": "bar"} != pf def test_propfile_eq_set_nochange(): pf = PropertiesFile.loads(INPUT) pf2 = PropertiesFile.loads(INPUT) assert pf == pf2 assert pf["key"] == pf2["key"] == "value" pf2["key"] = "value" assert pf == pf2 assert dict(pf) == dict(pf2) assert pf.dumps() == INPUT assert pf.dumps() != pf2.dumps() def test_propfile_neq_one_comment(): pf = PropertiesFile.loads('#This is a comment.\nkey=value\n') pf2 = PropertiesFile.loads('key=value\n') assert pf != pf2 assert dict(pf) == dict(pf2) def test_propfile_neq_different_comments(): pf = PropertiesFile.loads('#This is a comment.\nkey=value\n') pf2 = PropertiesFile.loads('#This is also a comment.\nkey=value\n') assert pf != pf2 assert dict(pf) == dict(pf2) def test_propfile_eq_one_repeated_key(): pf = PropertiesFile.loads('key = value\nkey: other value\n') pf2 = PropertiesFile.loads('key other value') assert pf == pf2 assert dict(pf) == dict(pf2) == {"key": "other value"} def test_propfile_eq_repeated_keys(): pf = PropertiesFile.loads('key = value\nkey: other value\n') pf2 = PropertiesFile.loads('key: whatever\nkey other value') assert pf == pf2 assert dict(pf) == dict(pf2) == {"key": "other value"} def test_propfile_neq_string(): pf = PropertiesFile.loads('key = value\nkey: other value\n') assert pf != 'key = value\nkey: other value\n' assert 'key = value\nkey: other value\n' != pf def test_propfile_preserve_trailing_escape(): pf = PropertiesFile.loads('key = value\\') pf._check() assert dict(pf) == {"key": "value"} assert pf.dumps() == 'key = value\\' def test_propfile_add_after_trailing_escape(): pf = PropertiesFile.loads('key = value\\') pf._check() pf["new"] = "old" pf._check() assert dict(pf) == {"key": "value", "new": "old"} assert pf.dumps() == 'key = value\nnew=old\n' def test_propfile_preserve_trailing_comment_escape(): pf = PropertiesFile.loads('#key = value\\') pf._check() assert dict(pf) == {} assert pf.dumps() == '#key = value\\' def test_propfile_add_after_trailing_comment_escape(): pf = PropertiesFile.loads('#key = value\\') pf._check() pf["new"] = "old" pf._check() assert dict(pf) == {"new": "old"} assert pf.dumps() == '#key = value\\\nnew=old\n' def test_propfile_preserve_no_trailing_newline(): pf = PropertiesFile.loads('key = value') pf._check() assert dict(pf) == {"key": "value"} assert pf.dumps() == 'key = value' def test_propfile_add_after_no_trailing_newline(): pf = PropertiesFile.loads('key = value\\') pf._check() pf["new"] = "old" pf._check() assert dict(pf) == {"key": "value", "new": "old"} assert pf.dumps() == 'key = value\nnew=old\n' def test_propfile_preserve_comment_no_trailing_newline(): pf = PropertiesFile.loads('#key = value') pf._check() assert dict(pf) == {} assert pf.dumps() == '#key = value' def test_propfile_add_after_comment_no_trailing_newline(): pf = PropertiesFile.loads('#key = value') pf._check() pf["new"] = "old" pf._check() assert dict(pf) == {"new": "old"} assert pf.dumps() == '#key = value\nnew=old\n' def test_propfile_preserve_trailing_escape_nl(): pf = PropertiesFile.loads('key = value\\\n') pf._check() assert dict(pf) == {"key": "value"} assert pf.dumps() == 'key = value\\\n' def test_propfile_add_after_trailing_escape_nl(): pf = PropertiesFile.loads('key = value\\\n') pf._check() pf["new"] = "old" pf._check() assert dict(pf) == {"key": "value", "new": "old"} assert pf.dumps() == 'key = value\nnew=old\n' def test_propfile_preserve_trailing_comment_escape_nl(): pf = PropertiesFile.loads('#key = value\\\n') pf._check() assert dict(pf) == {} assert pf.dumps() == '#key = value\\\n' def test_propfile_add_after_trailing_comment_escape_nl(): pf = PropertiesFile.loads('#key = value\\\n') pf._check() pf["new"] = "old" pf._check() assert dict(pf) == {"new": "old"} assert pf.dumps() == '#key = value\\\nnew=old\n' def test_propfile_get_nonstring_key(): pf = PropertiesFile({"key": "value", "apple": "zebra", "foo": "bar"}) with pytest.raises(TypeError) as excinfo: pf[42] assert str(excinfo.value) == \ 'Keys & values of PropertiesFile instances must be strings' def test_propfile_set_nonstring_key(): pf = PropertiesFile({"key": "value", "apple": "zebra", "foo": "bar"}) with pytest.raises(TypeError) as excinfo: pf[42] = 'forty-two' assert str(excinfo.value) == \ 'Keys & values of PropertiesFile instances must be strings' def test_propfile_set_nonstring_value(): pf = PropertiesFile({"key": "value", "apple": "zebra", "foo": "bar"}) with pytest.raises(TypeError) as excinfo: pf['forty-two'] = 42 assert str(excinfo.value) == \ 'Keys & values of PropertiesFile instances must be strings' def test_propfile_del_nonstring_key(): pf = PropertiesFile({"key": "value", "apple": "zebra", "foo": "bar"}) with pytest.raises(TypeError) as excinfo: del pf[42] assert str(excinfo.value) == \ 'Keys & values of PropertiesFile instances must be strings' def test_propfile_from_nonstring_key(): with pytest.raises(TypeError) as excinfo: PropertiesFile({"key": "value", 42: "forty-two"}) assert str(excinfo.value) == \ 'Keys & values of PropertiesFile instances must be strings' def test_propfile_from_nonstring_value(): with pytest.raises(TypeError) as excinfo: PropertiesFile({"key": "value", "forty-two": 42}) assert str(excinfo.value) == \ 'Keys & values of PropertiesFile instances must be strings' def test_propfile_empty_setitem(): pf = PropertiesFile() pf._check() pf["key"] = "value" pf._check() assert len(pf) == 1 assert bool(pf) assert dict(pf) == {"key": "value"} assert list(pf) == ["key"] assert list(reversed(pf)) == ["key"] assert pf.dumps() == 'key=value\n' def test_propfile_to_ordereddict(): pf = PropertiesFile.loads(INPUT) pf._check() assert OrderedDict(pf) == OrderedDict([ ("foo", "second definition"), ("bar", "only definition"), ("key", "value"), ("zebra", "apple"), ]) # preserving mixtures of line endings javaproperties-0.6.0/test/test_to_comment.py000066400000000000000000000106001362623413500213110ustar00rootroot00000000000000from __future__ import unicode_literals import pytest from six import unichr from javaproperties import to_comment # All C0 and C1 control characters other than \n and \r: s = ''.join(unichr(i) for i in list(range(0x20)) + list(range(0x7F, 0xA0)) if i not in (10, 13)) @pytest.mark.parametrize('cin,cout', [ ('', '#'), ('foobar', '#foobar'), (' leading', '# leading'), ('trailing ', '#trailing '), (' ', '# '), ('This is a comment.', '#This is a comment.'), ('#This is a double comment.', '##This is a double comment.'), ('trailing newline\n', '#trailing newline\n#'), ('trailing CRLF\r\n', '#trailing CRLF\n#'), ('trailing carriage return\r', '#trailing carriage return\n#'), ('line one\nline two', '#line one\n#line two'), ('line one\n#line two', '#line one\n#line two'), ('line one\n!line two', '#line one\n!line two'), ('\0', '#\0'), ('\a', '#\a'), ('\b', '#\b'), ('\t', '#\t'), ('\n', '#\n#'), ('\v', '#\v'), ('\f', '#\f'), ('\r', '#\n#'), ('\x1B', '#\x1B'), ('\x1F', '#\x1F'), ('!', '#!'), ('#', '##'), (':', '#:'), ('=', '#='), ('\\', '#\\'), ('\\u2603', '#\\u2603'), ('~', '#~'), ('\x7F', '#\x7F'), ('\x80', '#\x80'), ('\xA0', '#\xA0'), ('\xF0', '#\xF0'), ('\xFF', '#\xFF'), ('\u0100', '#\\u0100'), ('\u2603', '#\\u2603'), ('\U0001F410', '#\\ud83d\\udc10'), ('\uDC10\uD83D', '#\\udc10\\ud83d'), (s, '#' + s), ]) def test_to_comment(cin, cout): assert to_comment(cin) == cout assert to_comment(cin, ensure_ascii=None) == cout @pytest.mark.parametrize('cin,cout', [ ('', '#'), ('foobar', '#foobar'), (' leading', '# leading'), ('trailing ', '#trailing '), (' ', '# '), ('This is a comment.', '#This is a comment.'), ('#This is a double comment.', '##This is a double comment.'), ('trailing newline\n', '#trailing newline\n#'), ('trailing CRLF\r\n', '#trailing CRLF\n#'), ('trailing carriage return\r', '#trailing carriage return\n#'), ('line one\nline two', '#line one\n#line two'), ('line one\n#line two', '#line one\n#line two'), ('line one\n!line two', '#line one\n!line two'), ('\0', '#\0'), ('\a', '#\a'), ('\b', '#\b'), ('\t', '#\t'), ('\n', '#\n#'), ('\v', '#\v'), ('\f', '#\f'), ('\r', '#\n#'), ('\x1B', '#\x1B'), ('\x1F', '#\x1F'), ('!', '#!'), ('#', '##'), (':', '#:'), ('=', '#='), ('\\', '#\\'), ('\\u2603', '#\\u2603'), ('~', '#~'), ('\x7F', '#\x7F'), ('\x80', '#\x80'), ('\xA0', '#\xA0'), ('\xF0', '#\xF0'), ('\xFF', '#\xFF'), ('\u0100', '#\u0100'), ('\u2603', '#\u2603'), ('\U0001F410', '#\U0001F410'), ('\uDC10\uD83D', '#\uDC10\uD83D'), (s, '#' + s), ]) def test_to_comment_no_ensure_ascii(cin, cout): assert to_comment(cin, ensure_ascii=False) == cout @pytest.mark.parametrize('cin,cout', [ ('', '#'), ('foobar', '#foobar'), (' leading', '# leading'), ('trailing ', '#trailing '), (' ', '# '), ('This is a comment.', '#This is a comment.'), ('#This is a double comment.', '##This is a double comment.'), ('trailing newline\n', '#trailing newline\n#'), ('trailing CRLF\r\n', '#trailing CRLF\n#'), ('trailing carriage return\r', '#trailing carriage return\n#'), ('line one\nline two', '#line one\n#line two'), ('line one\n#line two', '#line one\n#line two'), ('line one\n!line two', '#line one\n!line two'), ('\0', '#\0'), ('\a', '#\a'), ('\b', '#\b'), ('\t', '#\t'), ('\n', '#\n#'), ('\v', '#\v'), ('\f', '#\f'), ('\r', '#\n#'), ('\x1B', '#\x1B'), ('\x1F', '#\x1F'), ('!', '#!'), ('#', '##'), (':', '#:'), ('=', '#='), ('\\', '#\\'), ('\\u2603', '#\\u2603'), ('~', '#~'), ('\x7F', '#\x7F'), ('\x80', '#\\u0080'), ('\xA0', '#\\u00a0'), ('\xF0', '#\\u00f0'), ('\xFF', '#\\u00ff'), ('\u0100', '#\\u0100'), ('\u2603', '#\\u2603'), ('\U0001F410', '#\\ud83d\\udc10'), ('\uDC10\uD83D', '#\\udc10\\ud83d'), ( s, '#' + ''.join(unichr(i) for i in list(range(0x20)) + [0x7F] if i not in (10, 13)) + ''.join('\\u{0:04x}'.format(i) for i in range(0x80, 0xA0)), ), ]) def test_to_comment_ensure_ascii(cin, cout): assert to_comment(cin, ensure_ascii=True) == cout javaproperties-0.6.0/test/test_unescape.py000066400000000000000000000033361362623413500207600ustar00rootroot00000000000000from __future__ import unicode_literals import pytest from javaproperties import InvalidUEscapeError, unescape @pytest.mark.parametrize('sin,sout', [ ('', ''), ('foobar', 'foobar'), (' space around ', ' space around '), ('\\ space\\ around\\ ', ' space around '), ('\\ \\ \\ ', ' '), ('\\u0000', '\0'), ('\\0', '0'), ('\\a', 'a'), ('\\b', 'b'), ('\\t', '\t'), ('\\n', '\n'), ('\\v', 'v'), ('\\f', '\f'), ('\\r', '\r'), ('\\e', 'e'), ('\\u001F', '\x1F'), ('\\q', 'q'), ('\\xF0', 'xF0'), ('\\!', '!'), ('\\#', '#'), ('\\:', ':'), ('\\=', '='), ('\\\\', '\\'), ('\\\\u2603', '\\u2603'), ('\\u007f', '\x7F'), ('\\u00f0', '\xF0'), ('\\u2603', '\u2603'), ('\\u012345678', '\u012345678'), ('\\uabcd', '\uABCD'), ('\\uABCD', '\uABCD'), ('\\ud83d\\udc10', '\U0001F410'), ('\\U0001f410', 'U0001f410'), ('\\udc10\\ud83d', '\uDC10\uD83D'), ('\0', '\0'), ('\t', '\t'), ('\n', '\n'), ('\x7F', '\x7F'), ('\xF0', '\xF0'), ('\u2603', '\u2603'), ('\U0001F410', '\U0001F410'), ]) def test_unescape(sin, sout): assert unescape(sin) == sout @pytest.mark.parametrize('s,esc', [ ('\\u', '\\u'), ('\\u ', '\\u '), ('\\ux', '\\ux'), ('\\uab', '\\uab'), ('\\uab\\cd', '\\uab\\c'), ('\\uab\\u0063d', '\\uab\\u'), ('\\uabc', '\\uabc'), ('\\uabc ', '\\uabc '), ('\\uabcx', '\\uabcx'), ('\\uxabc', '\\uxabc'), ('\\u abcx', '\\u abc'), ]) def test_unescape_invalid_u_escape(s, esc): with pytest.raises(InvalidUEscapeError) as excinfo: unescape(s) assert excinfo.value.escape == esc assert str(excinfo.value) == 'Invalid \\u escape sequence: ' + esc javaproperties-0.6.0/tox.ini000066400000000000000000000014421362623413500160740ustar00rootroot00000000000000[tox] envlist = py27,py34,py35,py36,py37,py38,pypy,pypy3 skip_missing_interpreters = True [testenv] setenv = LC_ALL=en_US.UTF-8 TZ=EST5EDT,M3.2.0,M11.1.0 usedevelop = True deps = pytest~=4.0 pytest-cov~=2.0 pytest-flakes~=4.0 pytest-mock~=1.6 python-dateutil~=2.6 commands = pytest {posargs} javaproperties test [pytest] addopts = --cache-clear --cov=javaproperties --doctest-modules --flakes doctest_optionflags = ALLOW_UNICODE filterwarnings = error # ignore::DeprecationWarning:xml.etree.ElementTree [coverage:run] branch = True [coverage:report] precision = 2 show_missing = True [testenv:docs] basepython = python3 deps = -rdocs/requirements.txt changedir = docs commands = sphinx-build -E -W -b html . _build/html