Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-tomli-w for openSUSE:Factory checked in at 2022-02-26 17:02:19 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-tomli-w (Old) and /work/SRC/openSUSE:Factory/.python-tomli-w.new.1958 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-tomli-w" Sat Feb 26 17:02:19 2022 rev:2 rq:957747 version:1.0.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-tomli-w/python-tomli-w.changes 2021-10-26 20:13:53.566016113 +0200 +++ /work/SRC/openSUSE:Factory/.python-tomli-w.new.1958/python-tomli-w.changes 2022-02-26 17:02:52.931543519 +0100 @@ -1,0 +2,19 @@ +Fri Feb 25 11:48:21 UTC 2022 - Ferdinand Thiessen <[email protected]> + +- Update to version 1.0.0 + * Removed support for Python 3.6 + * Positional arguments of dump and dumps can no longer be passed by keyword. + * Revised logic for when the "Array of Tables" syntax will be used. + AoT syntax is used when at least one of the tables needs multiple + lines, or a single line wider than 100 chars, when rendered inline. + * A nested structure no longer alone triggers the AoT syntax. +- Update to version 0.4.0 + * Added support for formatting Python tuples as TOML arrays. + * Fixed formatting of decimal.Decimal("inf"), + decimal.Decimal("-inf") and decimal.Decimal("nan"). + * A list of dicts is now rendered using the "Array of Tables" + syntax if at least one of the tables is a nested structure, + or at least one of the tables would need a line wider than 100 + chars when rendered inline. + +------------------------------------------------------------------- Old: ---- tomli-w-0.3.0-gh.tar.gz New: ---- tomli-w-1.0.0-gh.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-tomli-w.spec ++++++ --- /var/tmp/diff_new_pack.cNkBe1/_old 2022-02-26 17:02:53.443543600 +0100 +++ /var/tmp/diff_new_pack.cNkBe1/_new 2022-02-26 17:02:53.447543601 +0100 @@ -1,7 +1,7 @@ # -# spec file for package python-tomli +# spec file for package python-tomli-w # -# Copyright (c) 2021 SUSE LLC +# Copyright (c) 2022 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python3-%{**}} %define skip_python2 1 Name: python-tomli-w -Version: 0.3.0 +Version: 1.0.0 Release: 0 Summary: A lil' TOML writer License: MIT ++++++ tomli-w-0.3.0-gh.tar.gz -> tomli-w-1.0.0-gh.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/.bumpversion.cfg new/tomli-w-1.0.0/.bumpversion.cfg --- old/tomli-w-0.3.0/.bumpversion.cfg 2021-07-23 10:51:39.000000000 +0200 +++ new/tomli-w-1.0.0/.bumpversion.cfg 2021-12-02 00:48:42.000000000 +0100 @@ -2,7 +2,7 @@ commit = True tag = True tag_name = {new_version} -current_version = 0.3.0 +current_version = 1.0.0 [bumpversion:file:pyproject.toml] search = version = "{current_version}" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/.github/workflows/tests.yaml new/tomli-w-1.0.0/.github/workflows/tests.yaml --- old/tomli-w-0.3.0/.github/workflows/tests.yaml 2021-07-23 10:51:39.000000000 +0200 +++ new/tomli-w-1.0.0/.github/workflows/tests.yaml 2021-12-02 00:48:42.000000000 +0100 @@ -16,7 +16,7 @@ - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: '3.8' - name: Install pre-commit run: | @@ -32,9 +32,9 @@ runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [pypy-3.6, pypy-3.7, 3.6, 3.7, 3.8, 3.9, 3.10-dev] + python-version: ['pypy-3.7', '3.7', '3.8', '3.9', '3.10', '3.11-dev'] os: [ubuntu-latest, macos-latest, windows-latest] - continue-on-error: ${{ matrix.python-version == '3.10-dev' }} + continue-on-error: ${{ matrix.python-version == '3.11-dev' }} steps: - uses: actions/checkout@v2 @@ -56,8 +56,8 @@ pytest --cov --cov-fail-under=100 - name: Report coverage - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.6' - uses: codecov/codecov-action@v1 + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' + uses: codecov/codecov-action@v2 allgood: runs-on: ubuntu-latest @@ -69,21 +69,24 @@ pypi-publish: # Only publish if all other jobs succeed - needs: - - allgood + needs: [ allgood ] if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: 3.7 - - name: Install Flit + python-version: '3.7' + - name: Install build and publish tools + run: | + pip install build twine + - name: Build and check run: | - pip install "flit==3.2.0" - - name: Build and publish + rm -rf dist/ && python -m build + twine check --strict dist/* + - name: Publish run: | - flit publish + twine upload dist/* env: - FLIT_USERNAME: __token__ - FLIT_PASSWORD: ${{ secrets.PYPI_TOKEN }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/.pre-commit-config.yaml new/tomli-w-1.0.0/.pre-commit-config.yaml --- old/tomli-w-0.3.0/.pre-commit-config.yaml 2021-07-23 10:51:39.000000000 +0200 +++ new/tomli-w-1.0.0/.pre-commit-config.yaml 2021-12-02 00:48:42.000000000 +0100 @@ -1,6 +1,6 @@ repos: - repo: https://github.com/executablebooks/mdformat - rev: b9b885e183ca16670b6d4a5ef8058664395dec58 # frozen: 0.7.7 + rev: 427df9181bd4d8e65c1108b912ad47a81628f03b # frozen: 0.7.10 hooks: - id: mdformat additional_dependencies: @@ -16,11 +16,11 @@ - flake8-builtins - flake8-comprehensions - repo: https://github.com/PyCQA/isort - rev: 6e4281f018ff848226d8993596765b2285e1624f # frozen: 5.9.2 + rev: fd5ba70665a37ec301a1f714ed09336048b3be63 # frozen: 5.9.3 hooks: - id: isort - repo: https://github.com/psf/black - rev: 93c10bf9ebccf8d7cc686b0b9579f2e5e41c5328 # frozen: 21.6b0 + rev: 911470a610e47d9da5ea938b0887c3df62819b85 # frozen: 21.9b0 hooks: - id: black - repo: https://github.com/myint/docformatter @@ -38,7 +38,7 @@ - id: python-check-blanket-noqa - id: python-check-blanket-type-ignore - repo: https://github.com/PyCQA/flake8 - rev: dcd740bc0ebaf2b3d43e59a0060d157c97de13f3 # frozen: 3.9.2 + rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1 hooks: - id: flake8 additional_dependencies: @@ -46,7 +46,7 @@ - flake8-builtins - flake8-comprehensions - repo: https://github.com/pre-commit/mirrors-mypy - rev: 44afb68a9695d04030edc5cdc5a4fc4f17e4f9e2 # frozen: v0.910 + rev: 5cf22ccb774a8be8f47dfe4c1e8c4f177c608cbf # frozen: v0.910-1 hooks: - id: mypy args: ["--scripts-are-modules"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/CHANGELOG.md new/tomli-w-1.0.0/CHANGELOG.md --- old/tomli-w-0.3.0/CHANGELOG.md 2021-07-23 10:51:39.000000000 +0200 +++ new/tomli-w-1.0.0/CHANGELOG.md 2021-12-02 00:48:42.000000000 +0100 @@ -1,5 +1,28 @@ # Changelog +## 1.0.0 + +- Removed + - Support for Python 3.6 + - Positional arguments of `dump` and `dumps` can no longer be passed by keyword. +- Changed + - Revised logic for when the "Array of Tables" syntax will be used. + AoT syntax is used when at least one of the tables needs multiple lines, or a single line wider than 100 chars, when rendered inline. + A nested structure no longer alone triggers the AoT syntax. + +## 0.4.0 + +- Added + - Support for formatting Python `tuple`s as TOML arrays. +- Fixed + - Formatting of `decimal.Decimal("inf")`, `decimal.Decimal("-inf")` and `decimal.Decimal("nan")`. +- Changed + - A list of dicts is now rendered using the "Array of Tables" syntax + if at least one of the tables is a nested structure, + or at least one of the tables would need a line wider than 100 chars when rendered inline. + Thank you [Anderson Bravalheri](https://github.com/abravalheri) for the + [PR](https://github.com/hukkin/tomli-w/pull/15). + ## 0.3.0 - Changed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/README.md new/tomli-w-1.0.0/README.md --- old/tomli-w-0.3.0/README.md 2021-07-23 10:51:39.000000000 +0200 +++ new/tomli-w-1.0.0/README.md 2021-12-02 00:48:42.000000000 +0100 @@ -17,7 +17,9 @@ - [Write to file](#write-to-file) - [FAQ](#faq) - [Does Tomli-W sort the document?](#does-tomli-w-sort-the-document) - - [Does Tomli-W support writing documents with comments, custom whitespace, or other stylistic choices?](#does-tomli-w-support-writing-documents-with-comments-custom-whitespace-or-other-stylistic-choices) + - [Does Tomli-W support writing documents with comments or custom whitespace?](#does-tomli-w-support-writing-documents-with-comments-or-custom-whitespace) + - [Why does Tomli-W not write a multi-line string if the string value contains newlines?](#why-does-tomli-w-not-write-a-multi-line-string-if-the-string-value-contains-newlines) + - [Is Tomli-W output guaranteed to be valid TOML?](#is-tomli-w-output-guaranteed-to-be-valid-toml) <!-- mdformat-toc end --> @@ -71,6 +73,47 @@ No, but it respects sort order of the input data, so one could sort the content of the `dict` (recursively) before calling `tomli_w.dumps`. -### Does Tomli-W support writing documents with comments, custom whitespace, or other stylistic choices?<a name="does-tomli-w-support-writing-documents-with-comments-custom-whitespace-or-other-stylistic-choices"></a> +### Does Tomli-W support writing documents with comments or custom whitespace?<a name="does-tomli-w-support-writing-documents-with-comments-or-custom-whitespace"></a> No. + +### Why does Tomli-W not write a multi-line string if the string value contains newlines?<a name="why-does-tomli-w-not-write-a-multi-line-string-if-the-string-value-contains-newlines"></a> + +This default was chosen to achieve lossless parse/write round-trips. + +TOML strings can contain newlines where exact bytes matter, e.g. + +```toml +s = "here's a newline\r\n" +``` + +TOML strings also can contain newlines where exact byte representation is not relevant, e.g. + +```toml +s = """here's a newline +""" +``` + +A parse/write round-trip that converts the former example to the latter does not preserve the original newline byte sequence. +This is why Tomli-W avoids writing multi-line strings. + +A keyword argument is provided for users who do not need newline bytes to be preserved: + +```python +import tomli_w + +doc = {"s": "here's a newline\r\n"} +expected_toml = '''\ +s = """ +here's a newline +""" +''' +assert tomli_w.dumps(doc, multiline_strings=True) == expected_toml +``` + +### Is Tomli-W output guaranteed to be valid TOML?<a name="is-tomli-w-output-guaranteed-to-be-valid-toml"></a> + +No. +If there's a chance that your input data is bad and you need output validation, +parse the output string once with `tomli.loads`. +If the parse is successful (does not raise `tomli.TOMLDecodeError`) then the string is valid TOML. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/pyproject.toml new/tomli-w-1.0.0/pyproject.toml --- old/tomli-w-0.3.0/pyproject.toml 2021-07-23 10:51:39.000000000 +0200 +++ new/tomli-w-1.0.0/pyproject.toml 2021-12-02 00:48:42.000000000 +0100 @@ -4,13 +4,13 @@ [project] name = "tomli_w" -version = "0.3.0" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT +version = "1.0.0" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT description = "A lil' TOML writer" authors = [ { name = "Taneli Hukkinen", email = "[email protected]" }, ] license = { file = "LICENSE" } -requires-python = ">=3.6" +requires-python = ">=3.7" readme = "README.md" classifiers = [ "License :: OSI Approved :: MIT License", @@ -18,7 +18,6 @@ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -35,15 +34,6 @@ "Changelog" = "https://github.com/hukkin/tomli-w/blob/master/CHANGELOG.md" -[tool.flit.sdist] -exclude = [ - "tests/", - "benchmark/", - ".*", - "CHANGELOG.md", -] - - [tool.isort] # Force imports to be sorted by module, independent of import type force_sort_within_sections = true @@ -66,17 +56,17 @@ legacy_tox_ini = ''' [tox] # Only run pytest envs when no args given to tox -envlist = py{36,37,38,39} +envlist = py{37,38,39,310} isolated_build = True -[testenv:py{36,37,38,39}] +[testenv:py{37,38,39,310}] description = run tests against unpackaged source skip_install = True deps = -r tests/requirements.txt commands = pytest {posargs} -[testenv:py{36,37,38,39}-package] +[testenv:py{37,38,39,310}-package] description = run tests against a built package deps = -r tests/requirements.txt commands = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/tests/test_style.py new/tomli-w-1.0.0/tests/test_style.py --- old/tomli-w-0.3.0/tests/test_style.py 2021-07-23 10:51:39.000000000 +0200 +++ new/tomli-w-1.0.0/tests/test_style.py 2021-12-02 00:48:42.000000000 +0100 @@ -1,3 +1,5 @@ +import tomli + import tomli_w @@ -89,3 +91,184 @@ [a.b.c.d.e2.f2] """ assert actual == expected + + +def test_array_of_tables_containing_lists(): + example: dict = {"aot": [{"a": [0, 1, 2, 3]}]} + expected = """\ +[[aot]] +a = [ + 0, + 1, + 2, + 3, +] +""" + actual = tomli_w.dumps(example) + assert actual == expected + assert tomli.loads(actual) == example + + example = {"a": {"nested": example}} + expected = """\ +[[a.nested.aot]] +a = [ + 0, + 1, + 2, + 3, +] +""" + actual = tomli_w.dumps(example) + assert actual == expected + + +def test_array_of_long_tables(): + long_dict = { + "long-value": "Lorem ipsum sith", + "another-long-value": "consectetur adipis", + "simple-value": 3, + } + example = {"table": {"nested-array": [{"a": 42}, long_dict]}} + expected = """\ +[[table.nested-array]] +a = 42 + +[[table.nested-array]] +long-value = "Lorem ipsum sith" +another-long-value = "consectetur adipis" +simple-value = 3 +""" + actual = tomli_w.dumps(example) + assert actual == expected + assert tomli.loads(actual) == example + + +def test_array_of_short_tables(): + long_name = "a" * 87 + example = {"table": {"nested-array": [{long_name: 0}, {"b": 1}, {"c": 2}]}} + expected = f"""\ +[table] +nested-array = [ + {{ {long_name} = 0 }}, + {{ b = 1 }}, + {{ c = 2 }}, +] +""" + actual = tomli_w.dumps(example) + assert actual == expected + + +def test_example_issue_12(): + example = { + "table": { + "nested_table": [ + {"array_options": [1, 2, 3]}, + {"another_array": [1, 2]}, + {"c": 3}, + ] + } + } + expected = """\ +[[table.nested_table]] +array_options = [ + 1, + 2, + 3, +] + +[[table.nested_table]] +another_array = [ + 1, + 2, +] + +[[table.nested_table]] +c = 3 +""" + actual = tomli_w.dumps(example) + assert actual == expected + assert tomli.loads(actual) == example + + +def test_table_with_empty_array(): + # Empty arrays should never be AoTs + example: dict = {"table": {"array": []}} + expected = """\ +[table] +array = [] +""" + actual = tomli_w.dumps(example) + assert actual == expected + assert tomli.loads(actual) == example + + +def test_non_trivial_nesting(): + long = { + "long-value": "Lorem ipsum dolor sit amet", + "another-long-value": "consectetur adipiscing elit", + "a-third-one": "sed do eiusmod tempor incididunt ut labore et dolore magna", + "simple-value": 3, + } + example = { + "table": { + "aot": [ + {"nested-table": {"nested_aot": [{"a": [0, 1]}, {"b": 2}, {"c": 3}]}}, + {"other-nested-table": {"d": 4, "e": 5, "f": [{"g": 6}], "h": [long]}}, + ] + } + } + + expected = """\ +[[table.aot]] + +[[table.aot.nested-table.nested_aot]] +a = [ + 0, + 1, +] + +[[table.aot.nested-table.nested_aot]] +b = 2 + +[[table.aot.nested-table.nested_aot]] +c = 3 + +[[table.aot]] + +[table.aot.other-nested-table] +d = 4 +e = 5 +f = [ + { g = 6 }, +] + +[[table.aot.other-nested-table.h]] +long-value = "Lorem ipsum dolor sit amet" +another-long-value = "consectetur adipiscing elit" +a-third-one = "sed do eiusmod tempor incididunt ut labore et dolore magna" +simple-value = 3 +""" + actual = tomli_w.dumps(example) + assert actual == expected + assert tomli.loads(actual) == example + + +def test_multiline_in_aot(): + data = {"aot": [{"multiline_string": "line1\nline2"}]} + assert ( + tomli_w.dumps(data, multiline_strings=True) + == '''\ +[[aot]] +multiline_string = """ +line1 +line2""" +''' + ) + assert ( + tomli_w.dumps(data, multiline_strings=False) + == """\ +aot = [ + { multiline_string = "line1\\nline2" }, +] +""" + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/tests/test_types.py new/tomli-w-1.0.0/tests/test_types.py --- old/tomli-w-0.3.0/tests/test_types.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tomli-w-1.0.0/tests/test_types.py 2021-12-02 00:48:42.000000000 +0100 @@ -0,0 +1,40 @@ +from decimal import Decimal + +import tomli_w + + +def test_decimal(): + obj = { + "decimal-0": Decimal(0), + "decimal-pi": Decimal("3.14159"), + "decimal-inf": Decimal("inf"), + "decimal-minus-inf": Decimal("-inf"), + "decimal-nan": Decimal("nan"), + } + assert ( + tomli_w.dumps(obj) + == """\ +decimal-0 = 0 +decimal-pi = 3.14159 +decimal-inf = inf +decimal-minus-inf = -inf +decimal-nan = nan +""" + ) + + +def test_tuple(): + obj = {"empty-tuple": (), "non-empty-tuple": (1, (2, 3))} + assert ( + tomli_w.dumps(obj) + == """\ +empty-tuple = [] +non-empty-tuple = [ + 1, + [ + 2, + 3, + ], +] +""" + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/tomli_w/__init__.py new/tomli-w-1.0.0/tomli_w/__init__.py --- old/tomli-w-0.3.0/tomli_w/__init__.py 2021-07-23 10:51:39.000000000 +0200 +++ new/tomli-w-1.0.0/tomli_w/__init__.py 2021-12-02 00:48:42.000000000 +0100 @@ -1,4 +1,4 @@ __all__ = ("dumps", "dump") -__version__ = "0.3.0" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT +__version__ = "1.0.0" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT from tomli_w._writer import dump, dumps diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tomli-w-0.3.0/tomli_w/_writer.py new/tomli-w-1.0.0/tomli_w/_writer.py --- old/tomli-w-0.3.0/tomli_w/_writer.py 2021-07-23 10:51:39.000000000 +0200 +++ new/tomli-w-1.0.0/tomli_w/_writer.py 2021-12-02 00:48:42.000000000 +0100 @@ -1,13 +1,18 @@ +from __future__ import annotations + +from collections.abc import Generator, Mapping from datetime import date, datetime, time from decimal import Decimal import string from types import MappingProxyType -from typing import Any, BinaryIO, Dict, Generator, Mapping, NamedTuple +from typing import Any, BinaryIO, NamedTuple ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) ILLEGAL_BASIC_STR_CHARS = frozenset('"\\') | ASCII_CTRL - frozenset("\t") BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_") +ARRAY_TYPES = (list, tuple) ARRAY_INDENT = " " * 4 +MAX_LINE_LENGTH = 100 COMPACT_ESCAPES = MappingProxyType( { @@ -21,88 +26,126 @@ ) -def dump(obj: Dict[str, Any], fp: BinaryIO, *, multiline_strings: bool = False) -> None: - opts = Opts(multiline_strings) - for chunk in gen_table_chunks(obj, opts, name=""): - fp.write(chunk.encode()) +def dump( + __obj: dict[str, Any], __fp: BinaryIO, *, multiline_strings: bool = False +) -> None: + ctx = Context(multiline_strings, {}) + for chunk in gen_table_chunks(__obj, ctx, name=""): + __fp.write(chunk.encode()) -def dumps(obj: Dict[str, Any], *, multiline_strings: bool = False) -> str: - opts = Opts(multiline_strings) - return "".join(gen_table_chunks(obj, opts, name="")) +def dumps(__obj: dict[str, Any], *, multiline_strings: bool = False) -> str: + ctx = Context(multiline_strings, {}) + return "".join(gen_table_chunks(__obj, ctx, name="")) -class Opts(NamedTuple): +class Context(NamedTuple): allow_multiline: bool + # cache rendered inline tables (mapping from object id to rendered inline table) + inline_table_cache: dict[int, str] def gen_table_chunks( - table: Mapping[str, Any], opts: Opts, *, name: str + table: Mapping[str, Any], + ctx: Context, + *, + name: str, + inside_aot: bool = False, ) -> Generator[str, None, None]: yielded = False literals = [] - tables = [] + tables: list[tuple[str, Any, bool]] = [] # => [(key, value, inside_aot)] for k, v in table.items(): if isinstance(v, dict): - tables.append((k, v)) + tables.append((k, v, False)) + elif is_aot(v) and not all(is_suitable_inline_table(t, ctx) for t in v): + tables.extend((k, t, True) for t in v) else: literals.append((k, v)) - if name and (literals or not tables): + if inside_aot or name and (literals or not tables): yielded = True - yield f"[{name}]\n" + yield f"[[{name}]]\n" if inside_aot else f"[{name}]\n" if literals: yielded = True for k, v in literals: - yield f"{format_key_part(k)} = {format_literal(v, opts)}\n" + yield f"{format_key_part(k)} = {format_literal(v, ctx)}\n" - for k, v in tables: + for k, v, in_aot in tables: if yielded: yield "\n" else: yielded = True - yield from gen_table_chunks( - v, opts, name=f"{name}.{format_key_part(k)}" if name else format_key_part(k) - ) + key_part = format_key_part(k) + display_name = f"{name}.{key_part}" if name else key_part + yield from gen_table_chunks(v, ctx, name=display_name, inside_aot=in_aot) -def format_literal(obj: object, opts: Opts, *, nest_level: int = 0) -> str: +def format_literal(obj: object, ctx: Context, *, nest_level: int = 0) -> str: if isinstance(obj, bool): return "true" if obj else "false" - if isinstance(obj, (int, float, Decimal, date, datetime)): + if isinstance(obj, (int, float, date, datetime)): return str(obj) + if isinstance(obj, Decimal): + return format_decimal(obj) if isinstance(obj, time): if obj.tzinfo: raise ValueError("TOML does not support offset times") return str(obj) if isinstance(obj, str): - return format_string(obj, allow_multiline=opts.allow_multiline) - if isinstance(obj, list): - if not obj: - return "[]" - item_indent = ARRAY_INDENT * (1 + nest_level) - closing_bracket_indent = ARRAY_INDENT * nest_level - return ( - "[\n" - + ",\n".join( - item_indent + format_literal(item, opts, nest_level=nest_level + 1) - for item in obj - ) - + f",\n{closing_bracket_indent}]" - ) + return format_string(obj, allow_multiline=ctx.allow_multiline) + if isinstance(obj, ARRAY_TYPES): + return format_inline_array(obj, ctx, nest_level) if isinstance(obj, dict): - if not obj: - return "{}" - return ( + return format_inline_table(obj, ctx) + raise TypeError(f"Object of type {type(obj)} is not TOML serializable") + + +def format_decimal(obj: Decimal) -> str: + if obj.is_nan(): + return "nan" + if obj == Decimal("inf"): + return "inf" + if obj == Decimal("-inf"): + return "-inf" + return str(obj) + + +def format_inline_table(obj: dict, ctx: Context) -> str: + # check cache first + obj_id = id(obj) + if obj_id in ctx.inline_table_cache: + return ctx.inline_table_cache[obj_id] + + if not obj: + rendered = "{}" + else: + rendered = ( "{ " + ", ".join( - f"{format_key_part(k)} = {format_literal(v, opts)}" + f"{format_key_part(k)} = {format_literal(v, ctx)}" for k, v in obj.items() ) + " }" ) - raise TypeError(f"Object of type {type(obj)} is not TOML serializable") + ctx.inline_table_cache[obj_id] = rendered + return rendered + + +def format_inline_array(obj: tuple | list, ctx: Context, nest_level: int) -> str: + if not obj: + return "[]" + item_indent = ARRAY_INDENT * (1 + nest_level) + closing_bracket_indent = ARRAY_INDENT * nest_level + return ( + "[\n" + + ",\n".join( + item_indent + format_literal(item, ctx, nest_level=nest_level + 1) + for item in obj + ) + + f",\n{closing_bracket_indent}]" + ) def format_key_part(part: str) -> str: @@ -139,3 +182,18 @@ result += "\\u" + hex(ord(char))[2:].rjust(4, "0") seq_start = pos + 1 pos += 1 + + +def is_aot(obj: Any) -> bool: + """Decides if an object behaves as an array of tables (i.e. a nonempty list + of dicts).""" + return bool( + isinstance(obj, ARRAY_TYPES) and obj and all(isinstance(v, dict) for v in obj) + ) + + +def is_suitable_inline_table(obj: dict, ctx: Context) -> bool: + """Use heuristics to decide if the inline-style representation is a good + choice for a given table.""" + rendered_inline = f"{ARRAY_INDENT}{format_inline_table(obj, ctx)}," + return len(rendered_inline) <= MAX_LINE_LENGTH and "\n" not in rendered_inline
