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

Reply via email to