pax_global_header00006660000000000000000000000064147230553630014522gustar00rootroot0000000000000052 comment=84b477f8e9ebcbe94718f8f0451f1dd5bc3a8ac2 javaproperties-0.8.2/000077500000000000000000000000001472305536300145675ustar00rootroot00000000000000javaproperties-0.8.2/.github/000077500000000000000000000000001472305536300161275ustar00rootroot00000000000000javaproperties-0.8.2/.github/dependabot.yml000066400000000000000000000012121472305536300207530ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: / schedule: interval: weekly commit-message: prefix: "[python]" labels: - dependencies - d:python - package-ecosystem: pip directory: /docs versioning-strategy: increase-if-necessary schedule: interval: weekly commit-message: prefix: "[python+docs]" labels: - dependencies - d:python - package-ecosystem: github-actions directory: / schedule: interval: weekly commit-message: prefix: "[gh-actions]" include: scope labels: - dependencies - d:github-actions javaproperties-0.8.2/.github/workflows/000077500000000000000000000000001472305536300201645ustar00rootroot00000000000000javaproperties-0.8.2/.github/workflows/test.yml000066400000000000000000000027701472305536300216740ustar00rootroot00000000000000name: Test on: pull_request: push: branches: - master schedule: - cron: '0 6 * * *' concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name }} cancel-in-progress: true jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - '3.8' - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' - 'pypy-3.8' - 'pypy-3.9' - 'pypy-3.10' toxenv: [py] include: - python-version: '3.8' toxenv: lint - python-version: '3.8' toxenv: typing steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip wheel python -m pip install --upgrade --upgrade-strategy=eager coverage tox - name: Run tests run: tox -e ${{ matrix.toxenv }} - name: Generate XML coverage report if: matrix.toxenv == 'py' run: coverage xml - name: Upload coverage to Codecov if: matrix.toxenv == 'py' uses: codecov/codecov-action@v5 with: fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} name: ${{ matrix.python-version }} # vim:set et sts=2: javaproperties-0.8.2/.gitignore000066400000000000000000000000761472305536300165620ustar00rootroot00000000000000.coverage* .mypy_cache/ .tox/ __pycache__/ dist/ docs/_build/ javaproperties-0.8.2/.pre-commit-config.yaml000066400000000000000000000012361472305536300210520ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-added-large-files - id: check-json - id: check-toml - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black rev: 24.4.2 hooks: - id: black - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 rev: 7.0.0 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - flake8-builtins - flake8-unused-arguments exclude: ^test/data javaproperties-0.8.2/.readthedocs.yaml000066400000000000000000000003461472305536300200210ustar00rootroot00000000000000version: 2 formats: all python: install: - requirements: docs/requirements.txt - method: pip path: . build: os: ubuntu-22.04 tools: python: "3" sphinx: configuration: docs/conf.py fail_on_warning: true javaproperties-0.8.2/CHANGELOG.md000066400000000000000000000072641472305536300164110ustar00rootroot00000000000000v0.8.2 (2024-12-01) ------------------- - Drop support for Python 3.6 and 3.7 - Support Python 3.11, 3.12, and 3.13 - Migrated from setuptools to hatch v0.8.1 (2021-10-05) ------------------- - Fix a typing issue in Python 3.9 - Support Python 3.10 v0.8.0 (2020-11-28) ------------------- - Drop support for Python 2.7, 3.4, and 3.5 - Support Python 3.9 - `ensure_ascii` parameter added to `PropertiesFile.dump()` and `PropertiesFile.dumps()` - **Bugfix**: When parsing XML input, empty `` tags now produce an empty string as a value, not `None` - Added type annotations - `Properties` and `PropertiesFile` no longer raise `TypeError` when given a non-string key or value, as type correctness is now expected to be enforced through static type checking - The `PropertiesElement` classes returned by `parse()` are no longer subclasses of `namedtuple`, but they can still be iterated over to retrieve their fields like a tuple v0.7.0 (2020-03-09) ------------------- - `parse()` now accepts strings as input - **Breaking**: `parse()` now returns a generator of custom objects instead of triples of strings - Gave `PropertiesFile` a settable `timestamp` property - Gave `PropertiesFile` a settable `header_comment` property - Handle unescaping surrogate pairs on narrow Python builds 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` 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.8.2/LICENSE000066400000000000000000000021071472305536300155740ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016-2024 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.8.2/README.rst000066400000000000000000000106571472305536300162670ustar00rootroot00000000000000|repostatus| |ci-status| |coverage| |pyversions| |license| .. |repostatus| image:: https://www.repostatus.org/badges/latest/active.svg :target: https://www.repostatus.org/#active :alt: Project Status: Active - The project has reached a stable, usable state and is being actively developed. .. |ci-status| image:: https://github.com/jwodder/javaproperties/actions/workflows/test.yml/badge.svg :target: https://github.com/jwodder/javaproperties/actions/workflows/test.yml :alt: CI Status .. |coverage| image:: https://codecov.io/gh/jwodder/javaproperties/branch/master/graph/badge.svg :target: https://codecov.io/gh/jwodder/javaproperties .. |pyversions| image:: https://img.shields.io/pypi/pyversions/javaproperties.svg :target: https://pypi.org/project/javaproperties .. |license| image:: https://img.shields.io/github/license/jwodder/javaproperties.svg?maxAge=2592000 :target: https://opensource.org/licenses/MIT :alt: MIT License `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 ============ ``javaproperties`` requires Python 3.8 or higher. Just use `pip `_ for Python 3 (You have pip, right?) to install it:: python3 -m 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.8.2/TODO.md000066400000000000000000000032411472305536300156560ustar00rootroot00000000000000- Write tests - Test reading & writing bytes - Run doctest on the README examples somehow? - Test `dump()` and `load()` - 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? - Properly annotate overloaded functions New Features ------------ - 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 - Add an equivalent of `parse()` for XML that can extract the comment? - Export `getproperties` and `setproperties` from `javaproperties-cli`? - `PropertiesFile`: - Support XML - Support getting, setting, & deleting individual 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? - When setting a key's value, add a way to specify whether to take the place of the first occurrence of the key or the last - Add a method for reformatting in-place? - Add a method for deleting all but a given set of keys? - Support setting the separator and `ensure_ascii` on a per-entry basis? - Give `escape()` an option (named `is_value`? `escape_spaces`?) for controlling whether to perform the more minimal escaping used for values rather than keys - Instead of accepting file-like objects with `readline()` methods, accept iterables of `str`/`bytes`? javaproperties-0.8.2/docs/000077500000000000000000000000001472305536300155175ustar00rootroot00000000000000javaproperties-0.8.2/docs/changelog.rst000066400000000000000000000076511472305536300202110ustar00rootroot00000000000000.. currentmodule:: javaproperties Changelog ========= v0.8.2 (2024-12-01) ------------------- - Drop support for Python 3.6 and 3.7 - Support Python 3.11, 3.12, and 3.13 - Migrated from setuptools to hatch v0.8.1 (2021-10-05) ------------------- - Fix a typing issue in Python 3.9 - Support Python 3.10 v0.8.0 (2020-11-28) ------------------- - Drop support for Python 2.7, 3.4, and 3.5 - Support Python 3.9 - ``ensure_ascii`` parameter added to `PropertiesFile.dump()` and `PropertiesFile.dumps()` - **Bugfix**: When parsing XML input, empty ```` tags now produce an empty string as a value, not `None` - Added type annotations - `Properties` and `PropertiesFile` no longer raise `TypeError` when given a non-string key or value, as type correctness is now expected to be enforced through static type checking - The `PropertiesElement` classes returned by `parse()` are no longer subclasses of `~collections.namedtuple`, but they can still be iterated over to retrieve their fields like a tuple v0.7.0 (2020-03-09) ------------------- - `parse()` now accepts strings as input - **Breaking**: `parse()` now returns a generator of custom objects instead of triples of strings - Gave `PropertiesFile` a settable `~PropertiesFile.timestamp` property - Gave `PropertiesFile` a settable `~PropertiesFile.header_comment` property - Handle unescaping surrogate pairs on narrow Python builds 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.8.2/docs/cli.rst000066400000000000000000000006221472305536300170200ustar00rootroot00000000000000Command-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.8.2/docs/conf.py000066400000000000000000000021241472305536300170150ustar00rootroot00000000000000from javaproperties import __version__ project = "javaproperties" author = "John T. Wodder II" copyright = "2016-2024 John T. Wodder II" # noqa: A001 extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_copybutton", ] 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" master_doc = "index" version = __version__ release = __version__ today_fmt = "%Y %b %d" default_role = "py:obj" pygments_style = "sphinx" html_theme = "sphinx_rtd_theme" html_theme_options = { "collapse_navigation": False, "prev_next_buttons_location": "both", } html_last_updated_fmt = "%Y %b %d" html_show_sourcelink = True html_show_sphinx = True html_show_copyright = True copybutton_prompt_text = r">>> |\.\.\. |\$ " copybutton_prompt_is_regexp = True javaproperties-0.8.2/docs/index.rst000066400000000000000000000074061472305536300173670ustar00rootroot00000000000000.. 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|_. Installation ============ ``javaproperties`` requires Python 3.8 or higher. Just use `pip `_ for Python 3 (You have pip, right?) to install it:: python3 -m 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.8.2/docs/plain.rst000066400000000000000000000105361472305536300173610ustar00rootroot00000000000000.. 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.8.2/docs/propclass.rst000066400000000000000000000001471472305536300202610ustar00rootroot00000000000000.. currentmodule:: javaproperties ``Properties`` Class ==================== .. autoclass:: Properties javaproperties-0.8.2/docs/propfile.rst000066400000000000000000000001631472305536300200710ustar00rootroot00000000000000.. currentmodule:: javaproperties ``PropertiesFile`` Class ======================== .. autoclass:: PropertiesFile javaproperties-0.8.2/docs/requirements.txt000066400000000000000000000000731472305536300210030ustar00rootroot00000000000000Sphinx~=8.0 sphinx-copybutton~=0.5.0 sphinx_rtd_theme~=3.0 javaproperties-0.8.2/docs/util.rst000066400000000000000000000040061472305536300172260ustar00rootroot00000000000000.. currentmodule:: javaproperties Low-Level Utilities =================== .. autofunction:: escape .. autofunction:: java_timestamp .. autofunction:: join_key_value .. autofunction:: to_comment .. autofunction:: unescape .. autoexception:: InvalidUEscapeError :show-inheritance: Low-Level Parsing ----------------- .. autofunction:: parse .. autoclass:: PropertiesElement .. autoclass:: Comment .. autoclass:: KeyValue .. autoclass:: Whitespace .. _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.8.2/docs/xmlprops.rst000066400000000000000000000023651472305536300201430ustar00rootroot00000000000000.. 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.8.2/pyproject.toml000066400000000000000000000041001472305536300174760ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "javaproperties" dynamic = ["version"] description = "Read & write Java .properties files" readme = "README.rst" requires-python = ">=3.8" license = "MIT" license-files = { paths = ["LICENSE"] } authors = [ { name = "John Thorvald Wodder II", email = "javaproperties@varonathe.org" } ] keywords = [ "java", "properties", "javaproperties", "configfile", "config", "configuration", ] classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "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", "Typing :: Typed", ] dependencies = [] [project.urls] "Source Code" = "https://github.com/jwodder/javaproperties" "Bug Tracker" = "https://github.com/jwodder/javaproperties/issues" "Documentation" = "https://javaproperties.readthedocs.io" [tool.hatch.version] path = "src/javaproperties/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/docs", "/src", "/test", "CHANGELOG.*", "CONTRIBUTORS.*", "tox.ini", ] [tool.hatch.envs.default] python = "3" [tool.mypy] allow_incomplete_defs = false allow_untyped_defs = false ignore_missing_imports = false # : no_implicit_optional = true implicit_reexport = false local_partial_types = true pretty = true show_error_codes = true show_traceback = true strict_equality = true warn_redundant_casts = true warn_return_any = true warn_unreachable = true javaproperties-0.8.2/src/000077500000000000000000000000001472305536300153565ustar00rootroot00000000000000javaproperties-0.8.2/src/javaproperties/000077500000000000000000000000001472305536300204145ustar00rootroot00000000000000javaproperties-0.8.2/src/javaproperties/__init__.py000066400000000000000000000031661472305536300225330ustar00rootroot00000000000000""" 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. """ import codecs from .propclass import Properties from .propfile import PropertiesFile from .reading import ( Comment, InvalidUEscapeError, KeyValue, PropertiesElement, Whitespace, load, loads, parse, unescape, ) from .writing import ( dump, dumps, escape, java_timestamp, javapropertiesreplace_errors, join_key_value, to_comment, ) from .xmlprops import dump_xml, dumps_xml, load_xml, loads_xml __version__ = "0.8.2" __author__ = "John Thorvald Wodder II" __author_email__ = "javaproperties@varonathe.org" __license__ = "MIT" __url__ = "https://github.com/jwodder/javaproperties" __all__ = [ "Comment", "InvalidUEscapeError", "KeyValue", "Properties", "PropertiesElement", "PropertiesFile", "Whitespace", "dump", "dump_xml", "dumps", "dumps_xml", "escape", "java_timestamp", "javapropertiesreplace_errors", "join_key_value", "load", "load_xml", "loads", "loads_xml", "parse", "to_comment", "unescape", ] codecs.register_error("javapropertiesreplace", javapropertiesreplace_errors) javaproperties-0.8.2/src/javaproperties/propclass.py000066400000000000000000000176001472305536300230000ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterable, Iterator, Mapping from typing import Any, BinaryIO, IO, MutableMapping, Optional, TextIO, TypeVar from .reading import load from .writing import dump from .xmlprops import dump_xml, load_xml T = TypeVar("T") class Properties(MutableMapping[str, str]): """ 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 `str` values. 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 Optional[Properties] defaults: a set of default properties that will be used as fallback for `getProperty` .. |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 | Mapping[str, str] | Iterable[tuple[str, str]] = None, defaults: Optional["Properties"] = None, ) -> None: self.data: dict[str, str] = {} #: 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: str) -> str: return self.data[key] def __setitem__(self, key: str, value: str) -> None: self.data[key] = value def __delitem__(self, key: str) -> None: del self.data[key] def __iter__(self) -> Iterator[str]: return iter(self.data) def __len__(self) -> int: return len(self.data) def __repr__(self) -> str: return ( "{0.__module__}.{0.__name__}({1.data!r}, defaults={1.defaults!r})".format( type(self), self ) ) def __eq__(self, other: Any) -> bool: 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 getProperty(self, key: str, defaultValue: Optional[T] = None) -> str | T | 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 str key: the key to look up the value of :param Any defaultValue: the value to return if ``key`` is not found in the `Properties` instance :rtype: str (if ``key`` was found) """ 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: IO) -> None: """ 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 IO inStream: the file from which to read the ``.properties`` document :return: `None` :raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence occurs in the input """ self.data.update(load(inStream)) def propertyNames(self) -> Iterator[str]: 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: Iterator[str] """ 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: str, value: str) -> None: """Equivalent to ``self[key] = value``""" self[key] = value def store(self, out: TextIO, comments: Optional[str] = None) -> None: """ Write the `Properties` instance's entries (in unspecified order) in ``.properties`` format to ``out``, including the current timestamp. :param TextIO 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 Optional[str] comments: If non-`None`, ``comments`` will be written to ``out`` as a comment before any other content :return: `None` """ dump(self.data, out, comments=comments) def stringPropertyNames(self) -> set[str]: r""" Returns a `set` of all keys in the `Properties` instance and its `defaults` (and its `defaults`\ ’s `defaults`, etc.) :rtype: set[str] """ names = set(self.data) if self.defaults is not None: names.update(self.defaults.stringPropertyNames()) return names def loadFromXML(self, inStream: IO) -> None: """ 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. :param IO inStream: the file from which to read the XML properties document :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: BinaryIO, comment: Optional[str] = None, encoding: str = "UTF-8", ) -> None: """ Write the `Properties` instance's entries (in unspecified order) in XML properties format to ``out``. :param BinaryIO out: a file-like object to write the properties to :param Optional[str] comment: if non-`None`, ``comment`` will be output as a ```` element before the ```` elements :param str 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) -> Properties: """ .. 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.8.2/src/javaproperties/propfile.py000066400000000000000000000473701472305536300226210ustar00rootroot00000000000000from __future__ import annotations from collections import OrderedDict from collections.abc import Iterable, Iterator, Mapping from datetime import datetime from io import BytesIO, StringIO from typing import Any, AnyStr, IO, MutableMapping, Optional, Reversible, TextIO, cast from .reading import Comment, KeyValue, PropertiesElement, Whitespace, loads, parse from .util import CONTINUED_RGX, LinkedList, LinkedListNode, ascii_splitlines from .writing import java_timestamp, join_key_value, to_comment _NOSOURCE = "" # .source value for new or modified KeyValue instances class PropertiesFile(MutableMapping[str, str]): """ .. 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 | Mapping[str, str] | Iterable[tuple[str, str]] = None, **kwargs: str, ) -> None: #: mapping from keys to list of LinkedListNode's in self._lines self._key2nodes: MutableMapping[ str, list[LinkedListNode[PropertiesElement]] ] = OrderedDict() #: linked list of PropertiesElement's in order of appearance in file self._lines: LinkedList[PropertiesElement] = LinkedList() if mapping is not None: self.update(mapping) self.update(kwargs) def _check(self) -> None: """ Assert the internal consistency of the instance's data structures. This method is for debugging only. """ for k, ns in self._key2nodes.items(): assert k is not None, "null key" assert ns, "Key does not map to any nodes" indices = [] for n in ns: ix = self._lines.find_node(n) assert ix is not None, "Key has node not in line list" indices.append(ix) assert isinstance(n.value, KeyValue), "Key maps to comment" assert n.value.key == k, "Key does not map to itself" assert n.value.value is not None, "Key has null value" assert indices == sorted(indices), "Key's nodes are not in order" for line in self._lines: if not isinstance(line, KeyValue): 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 != _NOSOURCE: assert loads(line.source) == { line.key: line.value }, "Key source does not deserialize to itself" assert line.key in self._key2nodes, "Key is missing from map" assert any( line is n.value for n in self._key2nodes[line.key] ), "Key does not map to itself" # pragma: no cover def __getitem__(self, key: str) -> str: pe = self._key2nodes[key][-1].value assert isinstance(pe, KeyValue) return pe.value def __setitem__(self, key: str, value: str) -> None: try: nodes = self._key2nodes[key] except KeyError: if self._lines.end is not None: # 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.end.value if not ( isinstance(lastline, KeyValue) and lastline.source == _NOSOURCE ): lastsrc = lastline.source if isinstance(lastline, KeyValue): lastsrc = CONTINUED_RGX.sub(r"\1", lastsrc) if not lastsrc.endswith(("\r", "\n")): lastsrc += "\n" self._lines.end.value = lastline._with_source(lastsrc) n = self._lines.append(KeyValue(key, value, _NOSOURCE)) self._key2nodes[key] = [n] 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. n0 = nodes.pop(0) for n2 in nodes: n2.unlink() self._key2nodes[key] = [n0] n0.value = KeyValue(key, value, _NOSOURCE) def __delitem__(self, key: str) -> None: for n in self._key2nodes.pop(key): n.unlink() def __iter__(self) -> Iterator[str]: return iter(self._key2nodes) def __reversed__(self) -> Iterator[str]: return reversed(cast(Reversible[str], self._key2nodes)) def __len__(self) -> int: return len(self._key2nodes) def _comparable(self) -> list[tuple[Optional[str], str]]: return [ (p.key, p.value) if isinstance(p, KeyValue) else (None, p.source) for n in self._lines.iternodes() for p in [n.value] ### TODO: Also include non-final repeated keys??? if not isinstance(p, KeyValue) or n is self._key2nodes[p.key][-1] ] def __eq__(self, other: Any) -> bool: if isinstance(other, PropertiesFile): return self._comparable() == other._comparable() ### TODO: Special-case OrderedDict? elif isinstance(other, Mapping): return dict(self) == other else: return NotImplemented @classmethod def load(cls, fp: IO) -> PropertiesFile: """ 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 IO fp: the file from which to read the ``.properties`` document :rtype: PropertiesFile :raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence occurs in the input """ obj = cls() for elem in parse(fp): n = obj._lines.append(elem) if isinstance(elem, KeyValue): obj._key2nodes.setdefault(elem.key, []).append(n) return obj @classmethod def loads(cls, s: AnyStr) -> PropertiesFile: """ 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 Union[str,bytes] 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, bytes): fp = BytesIO(s) else: fp = StringIO(s) return cls.load(fp) def dump(self, fp: TextIO, separator: str = "=", ensure_ascii: bool = True) -> None: """ 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 and ``ensure_ascii`` setting. All key-value pairs are output in the order they were defined, with new keys added to the end. .. versionchanged:: 0.8.0 ``ensure_ascii`` parameter added .. 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 TextIO 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 str 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. :param bool ensure_ascii: if true, all non-ASCII characters in new or modified key-value pairs will be replaced with ``\\uXXXX`` escape sequences in the output; if false, non-ASCII characters will be passed through as-is :return: `None` """ for line in self._lines: if isinstance(line, KeyValue) and line.source == _NOSOURCE: print( join_key_value( line.key, line.value, separator=separator, ensure_ascii=ensure_ascii, ), file=fp, ) else: fp.write(line.source) def dumps(self, separator: str = "=", ensure_ascii: bool = True) -> str: """ Convert the mapping to a `str` 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 and ``ensure_ascii`` setting. All key-value pairs are output in the order they were defined, with new keys added to the end. .. versionchanged:: 0.8.0 ``ensure_ascii`` parameter added .. 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 str 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. :param bool ensure_ascii: if true, all non-ASCII characters in new or modified key-value pairs will be replaced with ``\\uXXXX`` escape sequences in the output; if false, non-ASCII characters will be passed through as-is :rtype: str """ s = StringIO() self.dump(s, separator=separator, ensure_ascii=ensure_ascii) return s.getvalue() def copy(self) -> PropertiesFile: """Create a copy of the mapping, including formatting information""" dup = type(self)() for elem in self._lines: n = dup._lines.append(elem) if isinstance(elem, KeyValue): dup._key2nodes.setdefault(elem.key, []).append(n) return dup @property def timestamp(self) -> Optional[str]: """ .. versionadded:: 0.7.0 The value of the timestamp comment, with the comment marker, any whitespace leading up to it, and the trailing newline removed. The timestamp comment is the first comment that appears to be a valid timestamp as produced by Java 8's ``Date.toString()`` and that does not come after any key-value pairs; if there is no such comment, the value of this property is `None`. The timestamp can be changed by assigning to this property. Assigning a string ``s`` replaces the timestamp comment with the output of ``to_comment(s)``; no check is made as to whether the result is a valid timestamp comment. Assigning `None` or `False` causes the timestamp comment to be deleted (also achievable with ``del pf.timestamp``). Assigning any other value ``x`` replaces the timestamp comment with the output of ``to_comment(java_timestamp(x))``. >>> pf = PropertiesFile.loads('''\\ ... #This is a comment. ... #Tue Feb 25 19:13:27 EST 2020 ... key = value ... zebra: apple ... ''') >>> pf.timestamp 'Tue Feb 25 19:13:27 EST 2020' >>> pf.timestamp = 1234567890 >>> pf.timestamp 'Fri Feb 13 18:31:30 EST 2009' >>> print(pf.dumps(), end='') #This is a comment. #Fri Feb 13 18:31:30 EST 2009 key = value zebra: apple >>> del pf.timestamp >>> pf.timestamp is None True >>> print(pf.dumps(), end='') #This is a comment. key = value zebra: apple """ for elem in self._lines: if isinstance(elem, Comment) and elem.is_timestamp(): return elem.value elif isinstance(elem, KeyValue): return None return None @timestamp.setter def timestamp(self, value: str | None | bool | float | datetime) -> None: if value is not None and value is not False: if not isinstance(value, str): value = java_timestamp(value) comments = [Comment(c) for c in ascii_splitlines(to_comment(value) + "\n")] else: comments = [] for n in self._lines.iternodes(): if isinstance(n.value, Comment) and n.value.is_timestamp(): if comments: n.value = comments[0] for c in comments[1:]: n = n.insert_after(c) else: n.unlink() return elif isinstance(n.value, KeyValue): for c in comments: n.insert_before(c) return else: for c in comments: self._lines.append(c) @timestamp.deleter def timestamp(self) -> None: for n in self._lines.iternodes(): if isinstance(n.value, Comment) and n.value.is_timestamp(): n.unlink() return elif isinstance(n.value, KeyValue): return @property def header_comment(self) -> Optional[str]: """ .. versionadded:: 0.7.0 The concatenated values of all comments at the top of the file, up to (but not including) the first key-value pair or timestamp comment, whichever comes first. The comments are returned with comment markers and the whitespace leading up to them removed, with line endings changed to ``\\n``, and with the line ending on the final comment (if any) removed. Blank/all-whitespace lines among the comments are ignored. The header comment can be changed by assigning to this property. Assigning a string ``s`` causes everything before the first key-value pair or timestamp comment to be replaced by the output of ``to_comment(s)``. Assigning `None` causes the header comment to be deleted (also achievable with ``del pf.header_comment``). >>> pf = PropertiesFile.loads('''\\ ... #This is a comment. ... ! This is also a comment. ... #Tue Feb 25 19:13:27 EST 2020 ... key = value ... zebra: apple ... ''') >>> pf.header_comment 'This is a comment.\\n This is also a comment.' >>> pf.header_comment = 'New comment' >>> print(pf.dumps(), end='') #New comment #Tue Feb 25 19:13:27 EST 2020 key = value zebra: apple >>> del pf.header_comment >>> pf.header_comment is None True >>> print(pf.dumps(), end='') #Tue Feb 25 19:13:27 EST 2020 key = value zebra: apple """ comments = [] for elem in self._lines: if isinstance(elem, Whitespace): pass elif isinstance(elem, KeyValue): break else: assert isinstance(elem, Comment) if elem.is_timestamp(): break comments.append(elem.value) if comments: return "\n".join(comments) else: return None @header_comment.setter def header_comment(self, value: Optional[str]) -> None: if value is None: comments = [] else: comments = [Comment(c) for c in ascii_splitlines(to_comment(value) + "\n")] while self._lines.start is not None: n = self._lines.start if isinstance(n.value, KeyValue) or ( isinstance(n.value, Comment) and n.value.is_timestamp() ): break else: n.unlink() if self._lines.start is None: for c in comments: self._lines.append(c) else: n = self._lines.start for c in comments: n.insert_before(c) @header_comment.deleter def header_comment(self) -> None: while self._lines.start is not None: n = self._lines.start if isinstance(n.value, KeyValue) or ( isinstance(n.value, Comment) and n.value.is_timestamp() ): break else: n.unlink() javaproperties-0.8.2/src/javaproperties/py.typed000066400000000000000000000000001472305536300221010ustar00rootroot00000000000000javaproperties-0.8.2/src/javaproperties/reading.py000066400000000000000000000301551472305536300224030ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable, Iterator from io import BytesIO, StringIO import re from typing import Any, IO, Iterable, TypeVar, overload from .util import CONTINUED_RGX, ascii_splitlines T = TypeVar("T") @overload def load(fp: IO) -> dict[str, str]: ... @overload def load(fp: IO, object_pairs_hook: type[T]) -> T: ... @overload def load(fp: IO, object_pairs_hook: Callable[[Iterator[tuple[str, str]]], T]) -> T: ... def load(fp, object_pairs_hook=dict): # type: ignore[no-untyped-def] """ 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 IO fp: the file 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 """ return object_pairs_hook( (kv.key, kv.value) for kv in parse(fp) if isinstance(kv, KeyValue) ) @overload def loads(s: str | bytes) -> dict[str, str]: ... @overload def loads(s: str | bytes, object_pairs_hook: type[T]) -> T: ... @overload def loads( s: str | bytes, object_pairs_hook: Callable[[Iterator[tuple[str, str]]], T] ) -> T: ... def loads(s, object_pairs_hook=dict): # type: ignore[no-untyped-def] """ 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 Union[str,bytes] 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, bytes) else StringIO(s) return load(fp, object_pairs_hook=object_pairs_hook) TIMESTAMP_RGX = re.compile( r"\A[ \t\f]*[#!][ \t\f]*" r"(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)" r" (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)" r" (?:[012][0-9]|3[01])" r" (?:[01][0-9]|2[0-3]):[0-5][0-9]:(?:[0-5][0-9]|6[01])" r" (?:[A-Za-z_0-9]{3})?" r" [0-9]{4,}" r"[ \t\f]*\r?\n?\Z" ) class PropertiesElement(Iterable[str]): """ .. versionadded:: 0.7.0 Superclass of objects returned by `parse()` """ def __init__(self, source: str) -> None: #: The raw, unmodified input line (including trailing newlines) self.source: str = source def __iter__(self) -> Iterator[str]: return iter((self.source,)) def __eq__(self, other: Any) -> bool: if type(self) is type(other): return tuple(self) == tuple(other) else: return NotImplemented def __repr__(self) -> str: return "{0.__module__}.{0.__name__}(source={1.source!r})".format( type(self), self ) @property def source_stripped(self) -> str: """ Like `source`, but with the final trailing newline and line continuation (if any) removed """ s = self.source.rstrip("\r\n") if CONTINUED_RGX.search(s): s = s[:-1] return s def _with_source(self, newsource: str) -> PropertiesElement: return type(self)(source=newsource) class Comment(PropertiesElement): """ .. versionadded:: 0.7.0 Subclass of `PropertiesElement` representing a comment """ @property def value(self) -> str: """ Returns the contents of the comment, with the comment marker, any whitespace leading up to it, and the trailing newline removed """ s = self.source.lstrip(" \t\f") if s.startswith(("#", "!")): s = s[1:] return s.rstrip("\r\n") @property def source_stripped(self) -> str: """ Like `source`, but with the final trailing newline (if any) removed """ return self.source.rstrip("\r\n") def is_timestamp(self) -> bool: """ Returns `True` iff the comment's value appears to be a valid timestamp as produced by Java 8's ``Date.toString()`` """ return bool(TIMESTAMP_RGX.fullmatch(self.source)) class Whitespace(PropertiesElement): """ .. versionadded:: 0.7.0 Subclass of `PropertiesElement` representing a line that is either empty or contains only whitespace (and possibly some line continuations) """ class KeyValue(PropertiesElement): """ .. versionadded:: 0.7.0 Subclass of `PropertiesElement` representing a key-value entry """ def __init__(self, key: str, value: str, source: str): super().__init__(source=source) #: The entry's key, after processing escape sequences self.key: str = key #: The entry's value, after processing escape sequences self.value: str = value def __iter__(self) -> Iterator[str]: return iter((self.key, self.value, self.source)) def __repr__(self) -> str: return ( "{0.__module__}.{0.__name__}(key={1.key!r}, value={1.value!r}," " source={1.source!r})".format(type(self), self) ) def _with_source(self, newsource: str) -> KeyValue: return type(self)(key=self.key, value=self.value, source=newsource) COMMENT_RGX = re.compile(r"^[ \t\f]*[#!]") BLANK_RGX = re.compile(r"^[ \t\f]*\r?\n?\Z") SEPARATOR_RGX = re.compile(r"(? Iterator[PropertiesElement]: """ Parse the given data as a simple line-oriented ``.properties`` file and return a generator of `PropertiesElement` objects representing the key-value pairs (as `KeyValue` objects), comments (as `Comment` objects), and blank lines (as `Whitespace` objects) in the input in order of occurrence. If the same key appears multiple times in the input, a separate `KeyValue` object is emitted for each entry. ``src`` may be a text string, a bytes string, or a text or binary filehandle/file-like object supporting the `~io.IOBase.readline` method (with or without universal newlines enabled). Bytes input is decoded as Latin-1. .. versionchanged:: 0.5.0 Invalid ``\\uXXXX`` escape sequences will now cause an `InvalidUEscapeError` to be raised .. versionchanged:: 0.7.0 `parse()` now accepts strings as input, and it now returns a generator of custom objects instead of triples of strings :param src: the ``.properties`` document :type src: string or file-like object :rtype: Iterator[PropertiesElement] :raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence occurs in the input """ liter: Iterator[str] if isinstance(src, bytes): liter = iter(ascii_splitlines(src.decode("iso-8859-1"))) elif isinstance(src, str): liter = iter(ascii_splitlines(src)) else: fp: IO = src def lineiter() -> Iterator[str]: while True: line = fp.readline() ll: str if isinstance(line, bytes): ll = line.decode("iso-8859-1") else: ll = line if ll == "": return for ln in ascii_splitlines(ll): yield ln liter = lineiter() for source in liter: line = source if COMMENT_RGX.match(line): yield Comment(source) continue elif BLANK_RGX.match(line): yield Whitespace(source) continue line = line.lstrip(" \t\f").rstrip("\r\n") while CONTINUED_RGX.search(line): line = line[:-1] nextline = next(liter, "") source += nextline line += nextline.lstrip(" \t\f").rstrip("\r\n") if line == "": # series of otherwise-blank lines with continuations yield Whitespace(source) continue m = SEPARATOR_RGX.search(line) if m: yield KeyValue( unescape(line[: m.start(1)]), unescape(line[m.end() :]), source, ) else: yield KeyValue(unescape(line), "", source) SURROGATE_PAIR_RGX = re.compile(r"[\uD800-\uDBFF][\uDC00-\uDFFF]") ESCAPE_RGX = re.compile(r"\\(u.{0,4}|.)") U_ESCAPE_RGX = re.compile(r"^u[0-9A-Fa-f]{4}\Z") def unescape(field: str) -> str: """ Decode escape sequences in a ``.properties`` key or value. The following escape sequences are recognized:: \\t \\n \\f \\r \\uXXXX \\\\ If a backslash is followed by any other character, the backslash is dropped. In addition, any valid UTF-16 surrogate pairs in the string after escape-decoding are further decoded into the non-BMP characters they represent. (Invalid & isolated surrogate code points are left as-is.) .. versionchanged:: 0.5.0 Invalid ``\\uXXXX`` escape sequences will now cause an `InvalidUEscapeError` to be raised :param str field: the string to decode :rtype: str :raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence occurs in the input """ return SURROGATE_PAIR_RGX.sub(_unsurrogate, ESCAPE_RGX.sub(_unesc, field)) _unescapes = {"t": "\t", "n": "\n", "f": "\f", "r": "\r"} def _unesc(m: re.Match[str]) -> str: esc = m.group(1) if esc[0] == "u": if not U_ESCAPE_RGX.match(esc): # We can't rely on `int` failing, because it succeeds when `esc` # has trailing whitespace or a leading minus. raise InvalidUEscapeError("\\" + esc) return chr(int(esc[1:], 16)) else: return _unescapes.get(esc, esc) def _unsurrogate(m: re.Match[str]) -> str: c, d = map(ord, m.group()) uord = ((c - 0xD800) << 10) + (d - 0xDC00) + 0x10000 return chr(uord) class InvalidUEscapeError(ValueError): """ .. versionadded:: 0.5.0 Raised when an invalid ``\\uXXXX`` escape sequence (i.e., a ``\\u`` not immediately followed by four hexadecimal digits) is encountered in a simple line-oriented ``.properties`` file """ def __init__(self, escape: str) -> None: #: The invalid ``\uXXXX`` escape sequence encountered self.escape: str = escape def __str__(self) -> str: return "Invalid \\u escape sequence: " + self.escape javaproperties-0.8.2/src/javaproperties/util.py000066400000000000000000000064211472305536300217460ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterable, Iterator, Mapping import re from typing import Generic, Optional, TypeVar CONTINUED_RGX = re.compile(r"(? None: self.start: Optional["LinkedListNode[T]"] = None self.end: Optional["LinkedListNode[T]"] = None def __iter__(self) -> Iterator[T]: return (n.value for n in self.iternodes()) def iternodes(self) -> Iterator["LinkedListNode[T]"]: n = self.start while n is not None: yield n n = n.next def append(self, value: T) -> LinkedListNode[T]: n = LinkedListNode(value, self) if self.start is None: self.start = n else: assert self.end is not None self.end.next = n n.prev = self.end self.end = n return n def find_node(self, node: "LinkedListNode[T]") -> Optional[int]: for i, n in enumerate(self.iternodes()): if n is node: return i return None class LinkedListNode(Generic[T]): def __init__(self, value: T, lst: LinkedList[T]) -> None: self.value: T = value self.lst: LinkedList[T] = lst self.prev: Optional["LinkedListNode[T]"] = None self.next: Optional["LinkedListNode[T]"] = None def unlink(self) -> None: if self.prev is not None: self.prev.next = self.next if self.next is not None: self.next.prev = self.prev if self is self.lst.start: self.lst.start = self.next if self is self.lst.end: self.lst.end = self.prev def insert_after(self, value: T) -> LinkedListNode[T]: """Inserts a new node with value ``value`` after the node ``self``""" n = LinkedListNode(value, self.lst) n.prev = self n.next = self.next self.next = n if n.next is not None: n.next.prev = n else: assert self is self.lst.end self.lst.end = n return n def insert_before(self, value: T) -> LinkedListNode[T]: """ Inserts a new node with value ``value`` before the node ``self`` """ n = LinkedListNode(value, self.lst) n.next = self n.prev = self.prev self.prev = n if n.prev is not None: n.prev.next = n else: assert self is self.lst.start self.lst.start = n return n def itemize( kvs: Mapping[K, V] | Iterable[tuple[K, V]], sort_keys: bool = False, ) -> Iterable[tuple[K, V]]: items: Iterable[tuple[K, V]] if isinstance(kvs, Mapping): items = ((k, kvs[k]) for k in kvs) else: items = kvs if sort_keys: items = sorted(items) return items def ascii_splitlines(s: str) -> list[str]: """ Like `str.splitlines(True)`, except it only treats LF, CR LF, and CR as line endings """ lines = [] lastend = 0 for m in EOL_RGX.finditer(s): lines.append(s[lastend : m.end()]) lastend = m.end() if lastend < len(s): lines.append(s[lastend:]) return lines javaproperties-0.8.2/src/javaproperties/writing.py000066400000000000000000000317251472305536300224610ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterable, Mapping from datetime import datetime from io import StringIO import re import time from typing import Optional, TextIO from .util import itemize def dump( props: Mapping[str, str] | Iterable[tuple[str, str]], fp: TextIO, separator: str = "=", comments: Optional[str] = None, timestamp: None | bool | float | datetime = True, sort_keys: bool = False, ensure_ascii: bool = True, ensure_ascii_comments: Optional[bool] = None, ) -> None: """ Write a series of key-value pairs to a file in simple line-oriented ``.properties`` format. .. versionchanged:: 0.6.0 ``ensure_ascii`` and ``ensure_ascii_comments`` parameters added :param props: A mapping or iterable of ``(key, value)`` pairs to write to ``fp``. All keys and values in ``props`` must be `str` values. If ``sort_keys`` is `False`, the entries are output in iteration order. :param TextIO fp: A file-like object to write the values of ``props`` to. It must have been opened as a text file. :param str separator: The string to use for separating keys & values. Only ``" "``, ``"="``, and ``":"`` (possibly with added whitespace) should ever be used as the separator. :param Optional[str] comments: if non-`None`, ``comments`` will be written to ``fp`` as a comment before any other content :param timestamp: If neither `None` nor `False`, a timestamp in the form of ``Mon Sep 02 14:00:54 EDT 2016`` is written as a comment to ``fp`` after ``comments`` (if any) and before the key-value pairs. If ``timestamp`` is `True`, the current date & time is used. If it is a number, it is converted from seconds since the epoch to local time. If it is a `datetime.datetime` object, its value is used directly, with naΓ―ve objects assumed to be in the local timezone. :type timestamp: `None`, `bool`, number, or `datetime.datetime` :param bool sort_keys: if true, the elements of ``props`` are sorted lexicographically by key in the output :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 :param Optional[bool] ensure_ascii_comments: if true, all non-ASCII characters in ``comments`` 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 :return: `None` """ if comments is not None: print(to_comment(comments, ensure_ascii=ensure_ascii_comments), file=fp) if timestamp is not None and timestamp is not False: print(to_comment(java_timestamp(timestamp)), file=fp) for k, v in itemize(props, sort_keys=sort_keys): print( join_key_value(k, v, separator, ensure_ascii=ensure_ascii), file=fp, ) def dumps( props: Mapping[str, str] | Iterable[tuple[str, str]], separator: str = "=", comments: Optional[str] = None, timestamp: None | bool | float | datetime = True, sort_keys: bool = False, ensure_ascii: bool = True, ensure_ascii_comments: Optional[bool] = None, ) -> str: """ Convert a series of key-value pairs to a `str` in simple line-oriented ``.properties`` format. .. versionchanged:: 0.6.0 ``ensure_ascii`` and ``ensure_ascii_comments`` parameters added :param props: A mapping or iterable of ``(key, value)`` pairs to serialize. All keys and values in ``props`` must be `str` values. If ``sort_keys`` is `False`, the entries are output in iteration order. :param str separator: The string to use for separating keys & values. Only ``" "``, ``"="``, and ``":"`` (possibly with added whitespace) should ever be used as the separator. :param Optional[str] comments: if non-`None`, ``comments`` will be output as a comment before any other content :param timestamp: If neither `None` nor `False`, a timestamp in the form of ``Mon Sep 02 14:00:54 EDT 2016`` is output as a comment after ``comments`` (if any) and before the key-value pairs. If ``timestamp`` is `True`, the current date & time is used. If it is a number, it is converted from seconds since the epoch to local time. If it is a `datetime.datetime` object, its value is used directly, with naΓ―ve objects assumed to be in the local timezone. :type timestamp: `None`, `bool`, number, or `datetime.datetime` :param bool sort_keys: if true, the elements of ``props`` are sorted lexicographically by key in the output :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 :param Optional[bool] ensure_ascii_comments: if true, all non-ASCII characters in ``comments`` 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 """ s = StringIO() dump( props, s, separator=separator, comments=comments, timestamp=timestamp, sort_keys=sort_keys, ensure_ascii=ensure_ascii, ensure_ascii_comments=ensure_ascii_comments, ) return s.getvalue() NON_ASCII_RGX = re.compile(r"[^\x00-\x7F]") NON_LATIN1_RGX = re.compile(r"[^\x00-\xFF]") NEWLINE_OLD_COMMENT_RGX = re.compile(r"\n(?![#!])") NON_N_EOL_RGX = re.compile(r"\r\n?") def to_comment(comment: str, ensure_ascii: Optional[bool] = None) -> str: """ Convert a string to a ``.properties`` file comment. Non-Latin-1 or non-ASCII characters in the string may be escaped using ``\\uXXXX`` escapes (depending on the value of ``ensure_ascii``), a ``#`` is prepended to the string, any CR LF or CR line breaks in the string are converted to LF, and a ``#`` is inserted after any line break not already followed by a ``#`` or ``!``. No trailing newline is added. >>> 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 str comment: the string to convert to a comment :param Optional[bool] 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: str """ 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: str, value: str, separator: str = "=", ensure_ascii: bool = True, ) -> str: 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 str key: the key :param str value: the value :param str separator: the string to use for separating the key & value. Only ``" "``, ``"="``, and ``":"`` (possibly with added whitespace) should ever be used as the separator. :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: str """ # 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: re.Match[str]) -> str: c = m.group() try: return _escapes[c] except KeyError: return _to_u_escape(c) def _to_u_escape(c: str) -> str: co = ord(c) if co > 0xFFFF: # Does Python really not have a decent builtin way to calculate # surrogate pairs? assert co <= 0x10FFFF co -= 0x10000 return "\\u{0:04x}\\u{1:04x}".format(0xD800 + (co >> 10), 0xDC00 + (co & 0x3FF)) else: return f"\\u{co:04x}" NEEDS_ESCAPE_ASCII_RGX = re.compile(r"[^\x20-\x7E]|[\\#!=:]") NEEDS_ESCAPE_UNICODE_RGX = re.compile(r"[\x00-\x1F\x7F]|[\\#!=:]") def _base_escape(field: str, ensure_ascii: bool = True) -> str: rgx = NEEDS_ESCAPE_ASCII_RGX if ensure_ascii else NEEDS_ESCAPE_UNICODE_RGX return rgx.sub(_esc, field) def escape(field: str, ensure_ascii: bool = True) -> str: """ 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 str field: the string to escape :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: str """ 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: None | bool | float | datetime = True) -> str: """ .. 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: str .. |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: ### TODO: Reimplement this using datetime.astimezone() to convert ### everything to an aware datetime? ts: Optional[float] if timestamp is True: ts = None elif isinstance(timestamp, datetime): # Use `datetime.timestamp()`, as it (unlike `datetime.timetuple()`) # takes `fold` into account for naΓ―ve datetimes. ts = timestamp.timestamp() else: # If it's not a number, it's localtime()'s problem now. ts = timestamp timebits = time.localtime(ts) tzname = timebits.tm_zone 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: UnicodeError) -> tuple[str, int]: """ .. 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 # pragma: no cover javaproperties-0.8.2/src/javaproperties/xmlprops.py000066400000000000000000000163301472305536300226550ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable, Iterable, Iterator, Mapping from typing import AnyStr, BinaryIO, IO, Optional, TypeVar, overload import xml.etree.ElementTree as ET from xml.sax.saxutils import escape, quoteattr from .util import itemize T = TypeVar("T") @overload def load_xml(fp: IO) -> dict[str, str]: ... @overload def load_xml(fp: IO, object_pairs_hook: type[T]) -> T: ... @overload def load_xml( fp: IO, object_pairs_hook: Callable[[Iterator[tuple[str, str]]], T] ) -> T: ... def load_xml(fp, object_pairs_hook=dict): # type: ignore[no-untyped-def] 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``. :param IO fp: the file 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 """ tree = ET.parse(fp) return object_pairs_hook(_fromXML(tree.getroot())) @overload def loads_xml(s: AnyStr) -> dict[str, str]: ... @overload def loads_xml(fp: IO, object_pairs_hook: type[T]) -> T: ... @overload def loads_xml( s: AnyStr, object_pairs_hook: Callable[[Iterator[tuple[str, str]]], T] ) -> T: ... def loads_xml(s, object_pairs_hook=dict): # type: ignore[no-untyped-def] 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``. :param Union[str,bytes] 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: ET.Element) -> Iterator[tuple[str, str]]: 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 or "") def dump_xml( props: Mapping[str, str] | Iterable[tuple[str, str]], fp: BinaryIO, comment: Optional[str] = None, encoding: str = "UTF-8", sort_keys: bool = False, ) -> None: """ 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 `str` values. If ``sort_keys`` is `False`, the entries are output in iteration order. :param BinaryIO fp: a file-like object to write the values of ``props`` to :param Optional[str] comment: if non-`None`, ``comment`` will be output as a ```` element before the ```` elements :param str 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` """ # This gives type errors : # fptxt = codecs.lookup(encoding).streamwriter(fp, errors='xmlcharrefreplace') # print('' # .format(quoteattr(encoding)), file=fptxt) # for s in _stream_xml(props, comment, sort_keys): # print(s, file=fptxt) fp.write( '\n'.format( quoteattr(encoding) ).encode(encoding, "xmlcharrefreplace") ) for s in _stream_xml(props, comment, sort_keys): fp.write((s + "\n").encode(encoding, "xmlcharrefreplace")) def dumps_xml( props: Mapping[str, str] | Iterable[tuple[str, str]], comment: Optional[str] = None, sort_keys: bool = False, ) -> str: """ Convert a series ``props`` of key-value pairs to a `str` 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 `str` values. If ``sort_keys`` is `False`, the entries are output in iteration order. :param Optional[str] comment: if non-`None`, ``comment`` will be output as a ```` element before the ```` elements :param bool sort_keys: if true, the elements of ``props`` are sorted lexicographically by key in the output :rtype: str """ return "".join(s + "\n" for s in _stream_xml(props, comment, sort_keys)) def _stream_xml( props: Mapping[str, str] | Iterable[tuple[str, str]], comment: Optional[str] = None, sort_keys: bool = False, ) -> Iterator[str]: 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.8.2/test/000077500000000000000000000000001472305536300155465ustar00rootroot00000000000000javaproperties-0.8.2/test/conftest.py000066400000000000000000000003571472305536300177520ustar00rootroot00000000000000import time import pytest @pytest.fixture def fixed_timestamp(mocker): mocker.patch("time.localtime", return_value=time.localtime(1478550580)) yield "Mon Nov 07 15:29:40 EST 2016" time.localtime.assert_called_once_with(None) javaproperties-0.8.2/test/test_dump_xml.py000066400000000000000000000017041472305536300210060ustar00rootroot00000000000000from io import BytesIO import pytest 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.8.2/test/test_dumps.py000066400000000000000000000155311472305536300203140ustar00rootroot00000000000000from collections import OrderedDict from datetime import datetime from dateutil.tz import tzoffset import pytest 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( chr(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.8.2/test/test_dumps_xml.py000066400000000000000000000123351472305536300211730ustar00rootroot00000000000000from 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", ), ( [ ("key", "value"), ("edh", "\xF0"), ("snowman", "\u2603"), ("goat", "\U0001F410"), ], '\n' "\n" 'value\n' '\xF0\n' '\u2603\n' '\U0001F410\n' "\n", ), ( [ ("key", "value"), ("\xF0", "edh"), ("\u2603", "snowman"), ("\U0001F410", "goat"), ], '\n' "\n" 'value\n' 'edh\n' 'snowman\n' 'goat\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.8.2/test/test_escape.py000066400000000000000000000036341472305536300204250ustar00rootroot00000000000000import 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.8.2/test/test_java_timestamp.py000066400000000000000000000124721472305536300221710ustar00rootroot00000000000000from datetime import datetime import sys 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.xfail( hasattr(sys, "pypy_version_info") and sys.pypy_version_info[:3] < (7, 2, 0), reason="Broken on this version of PyPy", # 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.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(fixed_timestamp): assert java_timestamp() == fixed_timestamp def test_java_timestamp_dogfood_type_error(): with pytest.raises(TypeError): java_timestamp("Mon Dec 12 12:12:12 EST 2016") javaproperties-0.8.2/test/test_join_key_value.py000066400000000000000000000071351472305536300221700ustar00rootroot00000000000000import 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.8.2/test/test_jpreplace.py000066400000000000000000000065321472305536300211320ustar00rootroot00000000000000import 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", "utf-8", pytest.param( "utf-16be", marks=[ # 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( hasattr(sys, "pypy_version_info") and sys.pypy_version_info[:3] < (7, 2, 0), 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.8.2/test/test_load_xml.py000066400000000000000000000032471472305536300207640ustar00rootroot00000000000000from io import BytesIO import pytest 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.8.2/test/test_loads.py000066400000000000000000000164551472305536300202740ustar00rootroot00000000000000from 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.8.2/test/test_loads_xml.py000066400000000000000000000076411472305536300211510ustar00rootroot00000000000000from collections import OrderedDict import pytest from javaproperties import loads_xml @pytest.mark.parametrize( "s,d", [ ("", {}), ( 'value', {"key": "value"}, ), ( ' ', {"key": " "}, ), ( '\n', {"key": "\n"}, ), ( '', {"key": ""}, ), ( '', {"key": ""}, ), ( "" '\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.8.2/test/test_parse.py000066400000000000000000000147321472305536300203000ustar00rootroot00000000000000import pytest from javaproperties import Comment, KeyValue, Whitespace, parse @pytest.mark.parametrize( "s,objects", [ ("", []), ("\n", [Whitespace("\n")]), (" \n\t\n", [Whitespace(" \n"), Whitespace("\t\n")]), ("key=value\n", [KeyValue("key", "value", "key=value\n")]), ("\xF0=\u2603\n", [KeyValue("\xF0", "\u2603", "\xF0=\u2603\n")]), ("\\u00F0=\\u2603\n", [KeyValue("\xF0", "\u2603", "\\u00F0=\\u2603\n")]), (" key :\t value \n", [KeyValue("key", "value ", " key :\t value \n")]), ( "#This is a comment.\n" "# So is this.\n" "comment: no\n" " ! Also a comment\n", [ Comment("#This is a comment.\n"), Comment("# So is this.\n"), KeyValue("comment", "no", "comment: no\n"), Comment(" ! Also a comment\n"), ], ), ( "#Before blank\n" "\n" "#After blank\n" "\n" "before=blank\n" "\n" "after=blank\n", [ Comment("#Before blank\n"), Whitespace( "\n", ), Comment("#After blank\n"), Whitespace("\n"), KeyValue("before", "blank", "before=blank\n"), Whitespace("\n"), KeyValue("after", "blank", "after=blank\n"), ], ), ("key va\\\n lue\n", [KeyValue("key", "value", "key va\\\n lue\n")]), ("key va\\\n", [KeyValue("key", "va", "key va\\\n")]), ("key va\\", [KeyValue("key", "va", "key va\\")]), (" \\\n\t\\\r\n\f\\\r \n", [Whitespace(" \\\n\t\\\r\n\f\\\r \n")]), ( "key = v\\\n\ta\\\r\n\fl\\\r u\\\ne\n", [KeyValue("key", "value", "key = v\\\n\ta\\\r\n\fl\\\r u\\\ne\n")], ), ], ) def test_parse(s, objects): assert list(parse(s)) == objects def test_keyvalue_attributes(): kv = KeyValue("a", "b", "c") assert kv.key == "a" assert kv.value == "b" assert kv.source == "c" assert repr(kv) == "javaproperties.reading.KeyValue(key='a', value='b', source='c')" k, v, s = kv assert k == kv.key assert v == kv.value assert s == kv.source def test_comment_attributes(): c = Comment("a") assert c.source == "a" assert repr(c) == "javaproperties.reading.Comment(source='a')" (s,) = c assert s == c.source def test_whitespace_attributes(): ws = Whitespace("a") assert ws.source == "a" assert repr(ws) == "javaproperties.reading.Whitespace(source='a')" (s,) = ws assert s == ws.source @pytest.mark.parametrize( "c,is_t", [ ("#\n", False), ("#Mon Sep 26 14:57:44 EDT 2016", True), ("#Mon Sep 26 14:57:44 EDT 2016\n", True), (" # Mon Sep 26 14:57:44 EDT 2016\n", True), ("#Wed Dec 31 19:00:00 EST 1969\n", True), ("#Fri Jan 01 00:00:00 EST 2016\n", True), ("#Tue Feb 02 02:02:02 EST 2016\n", True), ("#Thu Mar 03 03:03:03 EST 2016\n", True), ("#Mon Apr 04 04:04:04 EDT 2016\n", True), ("#Thu May 05 05:05:05 EDT 2016\n", True), ("#Mon Jun 06 06:06:06 EDT 2016\n", True), ("#Thu Jul 07 07:07:07 EDT 2016\n", True), ("#Mon Aug 08 08:08:08 EDT 2016\n", True), ("#Fri Sep 09 09:09:09 EDT 2016\n", True), ("#Mon Oct 10 10:10:10 EDT 2016\n", True), ("#Fri Nov 11 11:11:11 EST 2016\n", True), ("#Mon Dec 12 12:12:12 EST 2016\n", True), ("#Sun Jan 03 06:00:00 EST 2016\n", True), ("#Mon Jan 04 00:00:00 EST 2016\n", True), ("#Tue Jan 05 01:00:00 EST 2016\n", True), ("#Wed Jan 06 02:00:00 EST 2016\n", True), ("#Thu Jan 07 03:00:00 EST 2016\n", True), ("#Fri Jan 08 04:00:00 EST 2016\n", True), ("#Sat Jan 09 05:00:00 EST 2016\n", True), ("#Mon Feb 29 03:14:15 EST 2016\n", True), ("#Fri May 13 13:13:13 EDT 2016\n", True), ("#Mon Sep 26 14:57:44 2016\n", True), ("#Sat Jan 09 05:00:60 EST 2016\n", True), ("#Sat Jan 09 05:00:61 EST 2016\n", True), ("#Mon Feb 32 03:14:15 EST 2016\n", False), ("#Sun Jan 3 06:00:00 EST 2016\n", False), ("#Sun Jan 03 6:00:00 EST 2016\n", False), ("#Sat Jan 09 05:00:62 EST 2016\n", False), ("#Sat Jan 09 24:00:00 EST 2016\n", False), ("#Sat Jan 09 05:60:00 EST 2016\n", False), ("#Mo MΓ€r 02 13:59:03 EST 2020\n", False), ], ) def test_comment_is_timestamp(c, is_t): assert Comment(c).is_timestamp() == is_t @pytest.mark.parametrize( "s,ss", [ ("key=value", "key=value"), ("key=value\n", "key=value"), ("key=value\r\n", "key=value"), ("key=value\r", "key=value"), ("key va\\\n", "key va"), ("key va\\\\\n", "key va\\\\"), ("key va\\\\\\\n", "key va\\\\"), ("key va\\", "key va"), ("key va\\\n \\", "key va\\\n "), ("key va\\\n \\\n", "key va\\\n "), ("key va\\\n\\", "key va\\\n"), ("key va\\\n\\\n", "key va\\\n"), ], ) def test_keyvalue_source_stripped(s, ss): assert KeyValue(None, None, s).source_stripped == ss @pytest.mark.parametrize( "s,ss", [ ("#comment", "#comment"), ("#comment\n", "#comment"), ("#comment\r\n", "#comment"), ("#comment\r", "#comment"), ("#comment\\\n", "#comment\\"), ], ) def test_comment_source_stripped(s, ss): assert Comment(s).source_stripped == ss @pytest.mark.parametrize( "s,ss", [ (" ", " "), ("\n", ""), ("\r\n", ""), ("\r", ""), ("\\", ""), ("\\\n", ""), ("\\\\\n", "\\\\"), ("\\\\\\\n", "\\\\"), ("\\\n \\", "\\\n "), ("\\\n \\\n", "\\\n "), ("\\\n\\", "\\\n"), ("\\\n\\\n", "\\\n"), ], ) def test_whitespace_source_stripped(s, ss): assert Whitespace(s).source_stripped == ss @pytest.mark.parametrize( "s,v", [ ("#comment", "comment"), ("#comment\n", "comment"), ("!comment\n", "comment"), (" #comment\n", "comment"), ("# comment\n", " comment"), (" # comment\n", " comment"), ("\t#comment\n", "comment"), ("#\tcomment\n", "\tcomment"), ("\t#\tcomment\n", "\tcomment"), ("#comment value \n", "comment value "), (" weird edge # case", "weird edge # case"), ], ) def test_comment_value(s, v): assert Comment(s).value == v javaproperties-0.8.2/test/test_propclass.py000066400000000000000000000465431472305536300212010ustar00rootroot00000000000000from collections.abc import Iterator from io import BytesIO, StringIO import pytest from javaproperties import Properties, dumps # 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(fixed_timestamp): p = Properties() assert len(p) == 0 assert not bool(p) assert dict(p) == {} s = StringIO() p.store(s) assert s.getvalue() == "#" + fixed_timestamp + "\n" assert list(p.items()) == [] 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", } assert list(p.items()) == [ ("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", } assert list(p.items()) == [ ("key", "value"), ("horse", "orange"), ("foo", "second definition"), ("bar", "only definition"), ("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", } assert list(p.items()) == [ ("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", } assert list(p.items()) == [ ("key", "value"), ("horse", "orange"), ("foo", "second definition"), ("bar", "only definition"), ("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", } assert list(p.items()) == [ ("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", } assert list(p.items()) == [ ("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", } assert list(p.items()) == [ ("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", } assert list(p.items()) == [ ("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"} assert list(p.items()) == [ ("key", "value"), ("apple", "zebra"), ] 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"} assert list(p.items()) == [ ("key", "value"), ("apple", "zebra"), ] 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"} assert list(p.items()) == list(p2.items()) == [("Foo", "bar")] p2["Foo"] = "gnusto" assert dict(p) == {"Foo": "bar"} assert list(p.items()) == [("Foo", "bar")] assert dict(p2) == {"Foo": "gnusto"} assert list(p2.items()) == [("Foo", "gnusto")] assert p != p2 p2["fOO"] = "quux" assert dict(p) == {"Foo": "bar"} assert list(p.items()) == [("Foo", "bar")] assert dict(p2) == {"Foo": "gnusto", "fOO": "quux"} assert list(p2.items()) == [("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_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(fixed_timestamp): 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() == "#" + fixed_timestamp + "\nkey=value\n" def test_propclass_store(fixed_timestamp): p = Properties({"key": "value"}) s = StringIO() p.store(s) assert s.getvalue() == "#" + fixed_timestamp + "\nkey=value\n" def test_propclass_store_comment(fixed_timestamp): p = Properties({"key": "value"}) s = StringIO() p.store(s, comments="Testing") assert s.getvalue() == "#Testing\n#" + fixed_timestamp + "\nkey=value\n" def test_propclass_store_defaults(fixed_timestamp): defs = Properties({"key": "lock", "horse": "orange"}) p = Properties({"key": "value"}, defaults=defs) s = StringIO() p.store(s) assert s.getvalue() == "#" + fixed_timestamp + "\nkey=value\n" 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" @pytest.mark.parametrize( "data", [ {}, {"foo": "bar"}, {"foo": "bar", "key": "value"}, ], ) @pytest.mark.parametrize( "defaults", [ None, Properties(), Properties({"zebra": "apple"}), ], ) def test_propclass_repr(data, defaults): p = Properties(data, defaults=defaults) assert repr(p) == ( f"javaproperties.propclass.Properties({data!r}, defaults={defaults!r})" ) def test_propclass_repr_noinit(): p = Properties() assert repr(p) == "javaproperties.propclass.Properties({}, defaults=None)" # defaults with defaults javaproperties-0.8.2/test/test_propfile.py000066400000000000000000000723471472305536300210140ustar00rootroot00000000000000from collections import OrderedDict from datetime import datetime from dateutil.tz import tzstr 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.items()) == [] assert list(pf) == [] assert list(reversed(pf)) == [] assert pf.dumps() == "" @pytest.mark.parametrize("src", [INPUT, INPUT.encode("iso-8859-1")]) def test_propfile_loads(src): pf = PropertiesFile.loads(src) 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.items()) == [ ("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.items()) == [ ("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.items()) == [ ("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.items()) == [ ("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.items()) == [ ("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.items()) == [ ("foo", "second definition"), ("bar", "only definition"), ("zebra", "apple"), ("key", "recreated"), ] 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.items()) == [ ("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.items()) == [ ("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.items()) == [ ("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.items()) == [("key", "value"), ("apple", "zebra")] 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.items()) == [("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.items()) == [("key", "value"), ("apple", "zebra")] 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.items()) == [("key", "lock"), ("apple", "zebra")] 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 """ ) @pytest.mark.parametrize("ensure_ascii", [False, True]) def test_propfile_dumps_ensure_ascii(ensure_ascii): txt = INPUT + "Γ°=edh\n" pf = PropertiesFile.loads(txt) pf._check() assert pf.dumps(ensure_ascii=ensure_ascii) == txt def test_propfile_set_dumps_ensure_ascii(): pf = PropertiesFile.loads(INPUT) pf._check() pf["Γ°"] = "edh" pf._check() assert pf.dumps(ensure_ascii=True) == INPUT + "\\u00f0=edh\n" def test_propfile_set_dumps_no_ensure_ascii(): pf = PropertiesFile.loads(INPUT) pf._check() pf["Γ°"] = "edh" pf._check() assert pf.dumps(ensure_ascii=False) == INPUT + "Γ°=edh\n" 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_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"), ] ) @pytest.mark.parametrize( "src,ts", [ ("", None), ("#Thu Mar 16 17:06:52 EDT 2017\n", "Thu Mar 16 17:06:52 EDT 2017"), ("!Thu Mar 16 17:06:52 EDT 2017\n", "Thu Mar 16 17:06:52 EDT 2017"), ("\n \r#Thu Mar 16 17:06:52 EDT 2017\n", "Thu Mar 16 17:06:52 EDT 2017"), (INPUT, "Thu Mar 16 17:06:52 EDT 2017"), ( "# comment 1\n!comment 2\n# Thu Mar 16 17:06:52 EDT 2017\n", " Thu Mar 16 17:06:52 EDT 2017", ), ("key=value\n#Thu Mar 16 17:06:52 EDT 2017\n", None), ( "#Thu Mar 16 17:06:52 EDT 2017\n#Tue Feb 25 19:13:27 EST 2020\n", "Thu Mar 16 17:06:52 EDT 2017", ), ], ) def test_propfile_get_timestamp(src, ts): pf = PropertiesFile.loads(src) pf._check() assert pf.timestamp == ts @pytest.mark.parametrize( "src,ts,ts2,result", [ ( "", "Thu Mar 16 17:06:52 EDT 2017", "Thu Mar 16 17:06:52 EDT 2017", "#Thu Mar 16 17:06:52 EDT 2017\n", ), ("", None, None, ""), ("", False, None, ""), ("", "", None, "#\n"), ( "key=value\n", 0, "Wed Dec 31 19:00:00 EST 1969", "#Wed Dec 31 19:00:00 EST 1969\nkey=value\n", ), ( "key=value\n", 1234567890, "Fri Feb 13 18:31:30 EST 2009", "#Fri Feb 13 18:31:30 EST 2009\nkey=value\n", ), ( "key=value\n", datetime(2020, 3, 4, 15, 57, 41), "Wed Mar 04 15:57:41 EST 2020", "#Wed Mar 04 15:57:41 EST 2020\nkey=value\n", ), ( "key=value\n", datetime(2020, 3, 4, 12, 57, 41, tzinfo=tzstr("PST8PDT,M4.1.0,M10.5.0")), "Wed Mar 04 12:57:41 PST 2020", "#Wed Mar 04 12:57:41 PST 2020\nkey=value\n", ), ("key=value\n", None, None, "key=value\n"), ("key=value\n", False, None, "key=value\n"), ("key=value\n", "", None, "#\nkey=value\n"), ("key=value\n", "Not a timestamp", None, "#Not a timestamp\nkey=value\n"), ("key=value\n", "Line 1\n", None, "#Line 1\n#\nkey=value\n"), ("key=value\n", "Line 1\nLine 2", None, "#Line 1\n#Line 2\nkey=value\n"), ("key=value\n", "Line 1\n#Line 2", None, "#Line 1\n#Line 2\nkey=value\n"), ("key=value\n", "Line 1\n!Line 2", None, "#Line 1\n!Line 2\nkey=value\n"), ( "#Comment\n" "#Thu Mar 16 17:06:52 EDT 2017\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", 1234567890, "Fri Feb 13 18:31:30 EST 2009", "#Comment\n" "#Fri Feb 13 18:31:30 EST 2009\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", ), ( "#Comment\n" "#Thu Mar 16 17:06:52 EDT 2017\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", None, "Wed Mar 04 12:57:41 PST 2020", "#Comment\n#Comment 2\n#Wed Mar 04 12:57:41 PST 2020\nkey=value\n", ), ( "#Comment\n" "#Thu Mar 16 17:06:52 EDT 2017\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", False, "Wed Mar 04 12:57:41 PST 2020", "#Comment\n#Comment 2\n#Wed Mar 04 12:57:41 PST 2020\nkey=value\n", ), ( "#Comment\n" "#Thu Mar 16 17:06:52 EDT 2017\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", "", "Wed Mar 04 12:57:41 PST 2020", "#Comment\n" "#\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", ), ( "#Comment\n" "#Thu Mar 16 17:06:52 EDT 2017\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", "Not a timestamp", "Wed Mar 04 12:57:41 PST 2020", "#Comment\n" "#Not a timestamp\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", ), ( "#Comment\n" "#Thu Mar 16 17:06:52 EDT 2017\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", "Line 1\n", "Wed Mar 04 12:57:41 PST 2020", "#Comment\n" "#Line 1\n" "#\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", ), ( "#Comment\n" "#Thu Mar 16 17:06:52 EDT 2017\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", "Line 1\nLine 2", "Wed Mar 04 12:57:41 PST 2020", "#Comment\n" "#Line 1\n" "#Line 2\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", ), ( "#Comment\n" "#Thu Mar 16 17:06:52 EDT 2017\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", "Line 1\n#Line 2", "Wed Mar 04 12:57:41 PST 2020", "#Comment\n" "#Line 1\n" "#Line 2\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", ), ( "#Comment\n" "#Thu Mar 16 17:06:52 EDT 2017\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", "Line 1\n!Line 2", "Wed Mar 04 12:57:41 PST 2020", "#Comment\n" "#Line 1\n" "!Line 2\n" "#Comment 2\n" "#Wed Mar 04 12:57:41 PST 2020\n" "key=value\n", ), ( "#Comment\n\n#Comment 2\nkey=value\n", 1234567890, "Fri Feb 13 18:31:30 EST 2009", "#Comment\n" "\n" "#Comment 2\n" "#Fri Feb 13 18:31:30 EST 2009\n" "key=value\n", ), ], ) def test_propfile_set_timestamp(src, ts, ts2, result): pf = PropertiesFile.loads(src) pf._check() pf.timestamp = ts pf._check() assert pf.timestamp == ts2 assert pf.dumps() == result def test_propfile_set_timestamp_now(fixed_timestamp): pf = PropertiesFile.loads("key=value\n") pf._check() pf.timestamp = True pf._check() assert pf.timestamp == fixed_timestamp assert pf.dumps() == "#" + fixed_timestamp + "\nkey=value\n" @pytest.mark.parametrize( "src,ts2,result", [ ("", None, ""), ("#Thu Mar 16 17:06:52 EDT 2017\n", None, ""), ("\n \r#Thu Mar 16 17:06:52 EDT 2017\n", None, "\n \r"), ( INPUT, None, "# A comment before the timestamp\n" "# A comment after the timestamp\n" "foo: first definition\n" "bar=only definition\n" "\n" "# Comment between values\n" "\n" "key = value\n" "\n" "zebra \\\n" " apple\n" "foo : second definition\n" "\n" "# Comment at end of file\n", ), ( "# comment 1\n!comment 2\n# Thu Mar 16 17:06:52 EDT 2017\n", None, "# comment 1\n!comment 2\n", ), ( "key=value\n#Thu Mar 16 17:06:52 EDT 2017\n", None, "key=value\n#Thu Mar 16 17:06:52 EDT 2017\n", ), ( "#Thu Mar 16 17:06:52 EDT 2017\n#Tue Feb 25 19:13:27 EST 2020\n", "Tue Feb 25 19:13:27 EST 2020", "#Tue Feb 25 19:13:27 EST 2020\n", ), ], ) def test_propfile_delete_timestamp(src, ts2, result): pf = PropertiesFile.loads(src) pf._check() del pf.timestamp pf._check() assert pf.timestamp == ts2 assert pf.dumps() == result @pytest.mark.parametrize( "src,c", [ ("", None), ("#\n", ""), ("#\n#comment\n", "\ncomment"), ("#comment\n#\n", "comment\n"), (INPUT, " A comment before the timestamp"), ( "# comment 1\n!comment 2\n# Thu Mar 16 17:06:52 EDT 2017\n", " comment 1\ncomment 2", ), ("# comment 1\n!comment 2\nkey=value\n", " comment 1\ncomment 2"), ("# comment 1\r\n!comment 2\nkey=value\n", " comment 1\ncomment 2"), ("# comment 1\r!comment 2\nkey=value\n", " comment 1\ncomment 2"), ("# comment 1\n\t\r\n !comment 2\nkey=value\n", " comment 1\ncomment 2"), ("# Thu Mar 16 17:06:52 EDT 2017\n# Comment\n", None), ("key=value\n# Comment\n", None), ], ) def test_propfile_get_header_comment(src, c): pf = PropertiesFile.loads(src) pf._check() assert pf.header_comment == c @pytest.mark.parametrize( "c,c2,csrc", [ (None, None, ""), ("", "", "#\n"), ("This is test text.", "This is test text.", "#This is test text.\n"), ("Line 1\n", "Line 1\n", "#Line 1\n#\n"), ("Line 1\nLine 2", "Line 1\nLine 2", "#Line 1\n#Line 2\n"), ("Line 1\n#Line 2", "Line 1\nLine 2", "#Line 1\n#Line 2\n"), ("Line 1\n!Line 2", "Line 1\nLine 2", "#Line 1\n!Line 2\n"), ], ) @pytest.mark.parametrize( "part1", [ "", "#This comment will be deleted.\n", "#This will be deleted.\n!This, too\n", "#This will be deleted.\n \r\n#And also that blank line in between.\n", "\n\n#This and the blank lines above will be deleted.\n", "#This and the blank lines below will be deleted.\n\n\n", ], ) @pytest.mark.parametrize( "part2", [ "", "key=value\n", "#Thu Mar 16 17:06:52 EDT 2017\nkey=value\nkey=value\n#Post-entry comment\n", ], ) def test_propfile_set_header_comment(part1, part2, c, c2, csrc): pf = PropertiesFile.loads(part1 + part2) pf._check() pf.header_comment = c pf._check() assert pf.header_comment == c2 assert pf.dumps() == csrc + part2 @pytest.mark.parametrize( "part1", [ "", "#This comment will be deleted.\n", "#This will be deleted.\n!This, too\n", "#This will be deleted.\n \r\n#And also that blank line in between.\n", "\n\n#This and the blank lines above will be deleted.\n", "#This and the blank lines below will be deleted.\n\n\n", ], ) @pytest.mark.parametrize( "part2", [ "", "key=value\n", "#Thu Mar 16 17:06:52 EDT 2017\nkey=value\nkey=value\n#Post-entry comment\n", ], ) def test_propfile_delete_header_comment(part1, part2): pf = PropertiesFile.loads(part1 + part2) pf._check() del pf.header_comment pf._check() assert pf.header_comment is None assert pf.dumps() == part2 # preserving mixtures of line endings javaproperties-0.8.2/test/test_to_comment.py000066400000000000000000000114371472305536300213310ustar00rootroot00000000000000import pytest from javaproperties import to_comment # All C0 and C1 control characters other than \n and \r: s = "".join( chr(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(chr(i) for i in list(range(0x20)) + [0x7F] if i not in (10, 13)) + "".join(f"\\u{i:04x}" for i in range(0x80, 0xA0)), ), ], ) def test_to_comment_ensure_ascii(cin, cout): assert to_comment(cin, ensure_ascii=True) == cout javaproperties-0.8.2/test/test_unescape.py000066400000000000000000000036321472305536300207660ustar00rootroot00000000000000import 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.8.2/test/test_util.py000066400000000000000000000175101472305536300201400ustar00rootroot00000000000000import pytest from javaproperties.util import LinkedList, ascii_splitlines def test_linkedlist_empty(): ll = LinkedList() assert list(ll) == [] assert list(ll.iternodes()) == [] assert ll.start is None assert ll.end is None def test_linkedlist_one_elem(): ll = LinkedList() n = ll.append(42) assert list(ll) == [42] assert list(ll.iternodes()) == [n] assert ll.find_node(n) == 0 assert ll.start is n assert ll.end is n assert n.prev is None assert n.next is None def test_linkedlist_two_elem(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") assert list(ll) == [42, "fnord"] assert list(ll.iternodes()) == [n1, n2] assert ll.find_node(n1) == 0 assert ll.find_node(n2) == 1 assert ll.start is n1 assert ll.end is n2 assert n1.prev is None assert n1.next is n2 assert n2.prev is n1 assert n2.next is None def test_linked_list_three_elem(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) assert list(ll) == [42, "fnord", [0, 1, 2]] assert list(ll.iternodes()) == [n1, n2, n3] assert ll.find_node(n1) == 0 assert ll.find_node(n2) == 1 assert ll.find_node(n3) == 2 assert ll.start is n1 assert ll.end is n3 assert n1.prev is None assert n1.next is n2 assert n2.prev is n1 assert n2.next is n3 assert n3.prev is n2 assert n3.next is None def test_linked_list_unlink_only(): ll = LinkedList() n = ll.append(42) n.unlink() assert list(ll) == [] assert list(ll.iternodes()) == [] assert ll.start is None assert ll.end is None assert ll.find_node(n) is None def test_linked_list_unlink_first(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) n1.unlink() assert list(ll) == ["fnord", [0, 1, 2]] assert list(ll.iternodes()) == [n2, n3] assert ll.find_node(n1) is None assert ll.find_node(n2) == 0 assert ll.find_node(n3) == 1 assert ll.start is n2 assert ll.end is n3 assert n2.prev is None assert n2.next is n3 assert n3.prev is n2 assert n3.next is None def test_linked_list_unlink_middle(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) n2.unlink() assert list(ll) == [42, [0, 1, 2]] assert list(ll.iternodes()) == [n1, n3] assert ll.find_node(n1) == 0 assert ll.find_node(n2) is None assert ll.find_node(n3) == 1 assert ll.start is n1 assert ll.end is n3 assert n1.prev is None assert n1.next is n3 assert n3.prev is n1 assert n3.next is None def test_linked_list_unlink_last(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) n3.unlink() assert list(ll) == [42, "fnord"] assert list(ll.iternodes()) == [n1, n2] assert ll.find_node(n1) == 0 assert ll.find_node(n2) == 1 assert ll.find_node(n3) is None assert ll.start is n1 assert ll.end is n2 assert n1.prev is None assert n1.next is n2 assert n2.prev is n1 assert n2.next is None def test_linked_list_insert_before_first(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) nx = n1.insert_before(3.14) assert list(ll) == [3.14, 42, "fnord", [0, 1, 2]] assert list(ll.iternodes()) == [nx, n1, n2, n3] assert ll.find_node(n1) == 1 assert ll.find_node(n2) == 2 assert ll.find_node(n3) == 3 assert ll.find_node(nx) == 0 assert ll.start is nx assert ll.end is n3 assert nx.prev is None assert nx.next is n1 assert n1.prev is nx assert n1.next is n2 assert n2.prev is n1 assert n2.next is n3 assert n3.prev is n2 assert n3.next is None def test_linked_list_insert_before_middle(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) nx = n2.insert_before(3.14) assert list(ll) == [42, 3.14, "fnord", [0, 1, 2]] assert list(ll.iternodes()) == [n1, nx, n2, n3] assert ll.find_node(n1) == 0 assert ll.find_node(n2) == 2 assert ll.find_node(n3) == 3 assert ll.find_node(nx) == 1 assert ll.start is n1 assert ll.end is n3 assert n1.prev is None assert n1.next is nx assert nx.prev is n1 assert nx.next is n2 assert n2.prev is nx assert n2.next is n3 assert n3.prev is n2 assert n3.next is None def test_linked_list_insert_before_last(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) nx = n3.insert_before(3.14) assert list(ll) == [42, "fnord", 3.14, [0, 1, 2]] assert list(ll.iternodes()) == [n1, n2, nx, n3] assert ll.find_node(n1) == 0 assert ll.find_node(n2) == 1 assert ll.find_node(n3) == 3 assert ll.find_node(nx) == 2 assert ll.start is n1 assert ll.end is n3 assert n1.prev is None assert n1.next is n2 assert n2.prev is n1 assert n2.next is nx assert nx.prev is n2 assert nx.next is n3 assert n3.prev is nx assert n3.next is None def test_linked_list_insert_after_first(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) nx = n1.insert_after(3.14) assert list(ll) == [42, 3.14, "fnord", [0, 1, 2]] assert list(ll.iternodes()) == [n1, nx, n2, n3] assert ll.find_node(n1) == 0 assert ll.find_node(n2) == 2 assert ll.find_node(n3) == 3 assert ll.find_node(nx) == 1 assert ll.start is n1 assert ll.end is n3 assert n1.prev is None assert n1.next is nx assert nx.prev is n1 assert nx.next is n2 assert n2.prev is nx assert n2.next is n3 assert n3.prev is n2 assert n3.next is None def test_linked_list_insert_after_middle(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) nx = n2.insert_after(3.14) assert list(ll) == [42, "fnord", 3.14, [0, 1, 2]] assert list(ll.iternodes()) == [n1, n2, nx, n3] assert ll.find_node(n1) == 0 assert ll.find_node(n2) == 1 assert ll.find_node(n3) == 3 assert ll.find_node(nx) == 2 assert ll.start is n1 assert ll.end is n3 assert n1.prev is None assert n1.next is n2 assert n2.prev is n1 assert n2.next is nx assert nx.prev is n2 assert nx.next is n3 assert n3.prev is nx assert n3.next is None def test_linked_list_insert_after_last(): ll = LinkedList() n1 = ll.append(42) n2 = ll.append("fnord") n3 = ll.append([0, 1, 2]) nx = n3.insert_after(3.14) assert list(ll) == [42, "fnord", [0, 1, 2], 3.14] assert list(ll.iternodes()) == [n1, n2, n3, nx] assert ll.find_node(n1) == 0 assert ll.find_node(n2) == 1 assert ll.find_node(n3) == 2 assert ll.find_node(nx) == 3 assert ll.start is n1 assert ll.end is nx assert n1.prev is None assert n1.next is n2 assert n2.prev is n1 assert n2.next is n3 assert n3.prev is n2 assert n3.next is nx assert nx.prev is n3 assert nx.next is None @pytest.mark.parametrize( "s,lines", [ ("", []), ("foobar", ["foobar"]), ("foo\n", ["foo\n"]), ("foo\r", ["foo\r"]), ("foo\r\n", ["foo\r\n"]), ("foo\n\r", ["foo\n", "\r"]), ("foo\nbar", ["foo\n", "bar"]), ("foo\rbar", ["foo\r", "bar"]), ("foo\r\nbar", ["foo\r\n", "bar"]), ("foo\n\rbar", ["foo\n", "\r", "bar"]), ( "Why\vare\fthere\x1Cso\x1Ddang\x1Emany\x85line\u2028separator\u2029" "characters?", [ "Why\vare\fthere\x1Cso\x1Ddang\x1Emany\x85line\u2028separator\u2029" "characters?" ], ), ], ) def test_ascii_splitlines(s, lines): assert ascii_splitlines(s) == lines javaproperties-0.8.2/tox.ini000066400000000000000000000032341472305536300161040ustar00rootroot00000000000000[tox] envlist = lint,typing,py38,py39,py310,py311,py312,py313,pypy3 skip_missing_interpreters = True isolated_build = True minversion = 3.3.0 [testenv] setenv = LC_ALL=en_US.UTF-8 TZ=EST5EDT,M3.2.0,M11.1.0 deps = coverage pytest pytest-mock python-dateutil commands = coverage erase coverage run -m pytest {posargs} --doctest-modules --pyargs javaproperties coverage run -m pytest {posargs} test coverage combine coverage report [testenv:lint] skip_install = True deps = flake8 flake8-bugbear flake8-builtins flake8-unused-arguments commands = flake8 src test [testenv:typing] deps = mypy commands = mypy src [pytest] filterwarnings = error # ignore:.*utcfromtimestamp.* is deprecated:DeprecationWarning:dateutil [coverage:run] branch = True parallel = True source = javaproperties [coverage:paths] source = src .tox/**/site-packages [coverage:report] precision = 2 show_missing = True exclude_lines = pragma: no cover @overload [flake8] doctests = True extend-exclude = build/,dist/,test/data,venv/ max-doc-length = 100 max-line-length = 100 unused-arguments-ignore-stub-functions = True extend-select = B901,B902,B950 ignore = A003,A005,B005,E203,E262,E266,E501,E704,U101,W503 [isort] atomic = True classes = IO force_sort_within_sections = True honor_noqa = True lines_between_sections = 0 profile = black reverse_relative = True sort_relative_in_force_sorted_sections = True src_paths = src [testenv:docs] basepython = python3 deps = -rdocs/requirements.txt changedir = docs commands = sphinx-build -E -W -b html . _build/html