Repository: sdispater/tomlkit
Pull request #41 (2019-03-06 01:03:46)

When subtracting two Date objects, a timedelta object is returned.
Here is the pull request that addresses #40. Let me know if there is anything else you need me to do!

codecov-io: # [Codecov](https://codecov.io/gh/sdispater/tomlkit/pull/41?src=pr&el=h1) Report
> Merging [#41](https://codecov.io/gh/sdispater/tomlkit/pull/41?src=pr&el=desc) into [master](https://codecov.io/gh/sdispater/tomlkit/commit/cad4e0a8aebe000d7009f103986d0f1b2dec7347?src=pr&el=desc) will **decrease** coverage by `0.04%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/sdispater/tomlkit/pull/41/graphs/tree.svg?width=650&token=ZE9lS2yLXi&height=150&src=pr)](https://codecov.io/gh/sdispater/tomlkit/pull/41?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #41      +/-   ##
==========================================
- Coverage   92.05%   92.01%   -0.05%     
==========================================
  Files          10       10              
  Lines        2040     2042       +2     
==========================================
+ Hits         1878     1879       +1     
- Misses        162      163       +1
```


| [Impacted Files](https://codecov.io/gh/sdispater/tomlkit/pull/41?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tomlkit/items.py](https://codecov.io/gh/sdispater/tomlkit/pull/41/diff?src=pr&el=tree#diff-dG9tbGtpdC9pdGVtcy5weQ==) | `87.77% <100%> (+0.03%)` | :arrow_up: |
| [tomlkit/parser.py](https://codecov.io/gh/sdispater/tomlkit/pull/41/diff?src=pr&el=tree#diff-dG9tbGtpdC9wYXJzZXIucHk=) | `96.1% <0%> (-0.16%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sdispater/tomlkit/pull/41?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sdispater/tomlkit/pull/41?src=pr&el=footer). Last update [cad4e0a...fbc1e6a](https://codecov.io/gh/sdispater/tomlkit/pull/41?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.travis.yml b/.travis.yml
index c62987f..2c94edb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,7 +32,8 @@ jobs:
     - python: "pypy3"
 
     - stage: linting
-      python: "3.6"
+      python: "3.7"
+      dist: xenial
       install:
         - pip install pre-commit
         - pre-commit install-hooks
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9acf18f..f40da72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,20 @@
 # Change Log
 
+## [Unreleased]
+
+### Fixed
+
+- Fixed the handling of inline tables.
+
+
 ## [0.5.3] - 2018-11-19
 
 ### Fixed
 
 - Fixed copy of TOML documents.
 - Fixed behavior on PyPy3.
+- Fixed date, datetime and time handling on Python 3.8.
+- Fixed behavior for sub table declaration with intermediate tables.
 
 
 ## [0.5.2] - 2018-11-09
diff --git a/tomlkit/_compat.py b/tomlkit/_compat.py
index b7407af..768ce33 100644
--- a/tomlkit/_compat.py
+++ b/tomlkit/_compat.py
@@ -67,12 +67,12 @@ except ImportError:
             if self._name is None:
                 return "%s.%s(%r)" % (
                     self.__class__.__module__,
-                    self.__class__.__qualname__,
+                    self.__class__.__name__,
                     self._offset,
                 )
             return "%s.%s(%r, %r)" % (
                 self.__class__.__module__,
-                self.__class__.__qualname__,
+                self.__class__.__name__,
                 self._offset,
                 self._name,
             )
@@ -137,6 +137,7 @@ except ImportError:
 
 PY2 = sys.version_info[0] == 2
 PY36 = sys.version_info >= (3, 6)
+PY38 = sys.version_info >= (3, 8)
 
 if PY2:
     unicode = unicode
diff --git a/tomlkit/api.py b/tomlkit/api.py
index e541c20..7936ce2 100644
--- a/tomlkit/api.py
+++ b/tomlkit/api.py
@@ -111,7 +111,7 @@ def table():  # type: () -> Table
 
 
 def inline_table():  # type: () -> InlineTable
-    return InlineTable(Container(), Trivia())
+    return InlineTable(Container(), Trivia(trail=""), new=True)
 
 
 def aot():  # type: () -> AoT
diff --git a/tomlkit/container.py b/tomlkit/container.py
index e1ff5fd..2a9974e 100644
--- a/tomlkit/container.py
+++ b/tomlkit/container.py
@@ -492,6 +492,15 @@ class Container(dict):
         for k, v in other.items():
             self[k] = v
 
+    def get(self, key, default=None):  # type: (Any, Optional[Any]) -> Any
+        if not isinstance(key, Key):
+            key = Key(key)
+
+        if key not in self:
+            return default
+
+        return self[key]
+
     def __contains__(self, key):  # type: (Union[Key, str]) -> bool
         if not isinstance(key, Key):
             key = Key(key)
@@ -522,7 +531,10 @@ class Container(dict):
 
         item = self._body[idx][1]
 
-        return item.value
+        if item.is_boolean():
+            return item.value
+
+        return item
 
     def __setitem__(self, key, value):  # type: (Union[Key, str], Any) -> None
         if key is not None and key in self:
diff --git a/tomlkit/items.py b/tomlkit/items.py
index 9ac0934..6293a42 100644
--- a/tomlkit/items.py
+++ b/tomlkit/items.py
@@ -16,6 +16,7 @@ from typing import Union
 
 
 from ._compat import PY2
+from ._compat import PY38
 from ._compat import decode
 from ._compat import long
 from ._compat import unicode
@@ -72,13 +73,32 @@ def item(value, _parent=None):
     elif isinstance(value, (str, unicode)):
         escaped = escape_string(value)
 
-        return String(StringType.SLB, value, escaped, Trivia())
+        return String(StringType.SLB, decode(value), escaped, Trivia())
     elif isinstance(value, datetime):
-        return DateTime(value, Trivia(), value.isoformat().replace("+00:00", "Z"))
+        return DateTime(
+            value.year,
+            value.month,
+            value.day,
+            value.hour,
+            value.minute,
+            value.second,
+            value.microsecond,
+            value.tzinfo,
+            Trivia(),
+            value.isoformat().replace("+00:00", "Z"),
+        )
     elif isinstance(value, date):
-        return Date(value, Trivia(), value.isoformat())
+        return Date(value.year, value.month, value.day, Trivia(), value.isoformat())
     elif isinstance(value, time):
-        return Time(value, Trivia(), value.isoformat())
+        return Time(
+            value.hour,
+            value.minute,
+            value.second,
+            value.microsecond,
+            value.tzinfo,
+            Trivia(),
+            value.isoformat(),
+        )
 
     raise ValueError("Invalid type {}".format(type(value)))
 
@@ -262,6 +282,15 @@ class Item(object):
 
         return self
 
+    def is_boolean(self):  # type: () -> bool
+        return isinstance(self, Bool)
+
+    def is_table(self):  # type: () -> bool
+        return isinstance(self, Table)
+
+    def is_inline_table(self):  # type: () -> bool
+        return isinstance(self, InlineTable)
+
     def _getstate(self, protocol=3):
         return (self._trivia,)
 
@@ -492,20 +521,36 @@ class DateTime(Item, datetime):
     A datetime literal.
     """
 
-    def __new__(cls, value, *_):  # type: (..., datetime, ...) -> datetime
+    def __new__(
+        cls,
+        year,
+        month,
+        day,
+        hour,
+        minute,
+        second,
+        microsecond,
+        tzinfo,
+        trivia,
+        raw,
+        **kwargs
+    ):  # type: (int, int, int, int, int, int, int, ..., Trivia, ...) -> datetime
         return datetime.__new__(
             cls,
-            value.year,
-            value.month,
-            value.day,
-            value.hour,
-            value.minute,
-            value.second,
-            value.microsecond,
-            tzinfo=value.tzinfo,
+            year,
+            month,
+            day,
+            hour,
+            minute,
+            second,
+            microsecond,
+            tzinfo=tzinfo,
+            **kwargs
         )
 
-    def __init__(self, _, trivia, raw):  # type: (datetime, Trivia, str) -> None
+    def __init__(
+        self, year, month, day, hour, minute, second, microsecond, tzinfo, trivia, raw
+    ):  # type: (int, int, int, int, int, int, int, ..., Trivia) -> None
         super(DateTime, self).__init__(trivia)
 
         self._raw = raw
@@ -522,12 +567,36 @@ class DateTime(Item, datetime):
         return self._raw
 
     def __add__(self, other):
-        result = super(DateTime, self).__add__(other)
+        if PY38:
+            result = datetime(
+                self.year,
+                self.month,
+                self.day,
+                self.hour,
+                self.minute,
+                self.second,
+                self.microsecond,
+                self.tzinfo,
+            ).__add__(other)
+        else:
+            result = super(DateTime, self).__add__(other)
 
         return self._new(result)
 
     def __sub__(self, other):
-        result = super(DateTime, self).__sub__(other)
+        if PY38:
+            result = datetime(
+                self.year,
+                self.month,
+                self.day,
+                self.hour,
+                self.minute,
+                self.second,
+                self.microsecond,
+                self.tzinfo,
+            ).__sub__(other)
+        else:
+            result = super(DateTime, self).__sub__(other)
 
         if isinstance(result, datetime):
             result = self._new(result)
@@ -537,20 +606,29 @@ class DateTime(Item, datetime):
     def _new(self, result):
         raw = result.isoformat()
 
-        return DateTime(result, self._trivia, raw)
+        return DateTime(
+            result.year,
+            result.month,
+            result.day,
+            result.hour,
+            result.minute,
+            result.second,
+            result.microsecond,
+            result.tzinfo,
+            self._trivia,
+            raw,
+        )
 
     def _getstate(self, protocol=3):
         return (
-            datetime(
-                self.year,
-                self.month,
-                self.day,
-                self.hour,
-                self.minute,
-                self.second,
-                self.microsecond,
-                self.tzinfo,
-            ),
+            self.year,
+            self.month,
+            self.day,
+            self.hour,
+            self.minute,
+            self.second,
+            self.microsecond,
+            self.tzinfo,
             self._trivia,
             self._raw,
         )
@@ -561,10 +639,12 @@ class Date(Item, date):
     A date literal.
     """
 
-    def __new__(cls, value, *_):  # type: (..., date, ...) -> date
-        return date.__new__(cls, value.year, value.month, value.day)
+    def __new__(cls, year, month, day, *_):  # type: (int, int, int, ...) -> date
+        return date.__new__(cls, year, month, day)
 
-    def __init__(self, _, trivia, raw):  # type: (date, Trivia, str) -> None
+    def __init__(
+        self, year, month, day, trivia, raw
+    ):  # type: (int, int, int, Trivia, str) -> None
         super(Date, self).__init__(trivia)
 
         self._raw = raw
@@ -581,22 +661,31 @@ class Date(Item, date):
         return self._raw
 
     def __add__(self, other):
-        result = super(Date, self).__add__(other)
+        if PY38:
+            result = date(self.year, self.month, self.day).__add__(other)
+        else:
+            result = super(Date, self).__add__(other)
 
         return self._new(result)
 
     def __sub__(self, other):
-        result = super(Date, self).__sub__(other)
+        if PY38:
+            result = date(self.year, self.month, self.day).__sub__(other)
+        else:
+            result = super(Date, self).__sub__(other)
 
-        return self._new(result)
+        if isinstance(result, date):
+            result = self._new(result)
+
+        return result
 
     def _new(self, result):
         raw = result.isoformat()
 
-        return Date(result, self._trivia, raw)
+        return Date(result.year, result.month, result.day, self._trivia, raw)
 
     def _getstate(self, protocol=3):
-        return (datetime(self.year, self.month, self.day), self._trivia, self._raw)
+        return (self.year, self.month, self.day, self._trivia, self._raw)
 
 
 class Time(Item, time):
@@ -604,12 +693,14 @@ class Time(Item, time):
     A time literal.
     """
 
-    def __new__(cls, value, *_):  # type: (time, ...) -> time
-        return time.__new__(
-            cls, value.hour, value.minute, value.second, value.microsecond
-        )
+    def __new__(
+        cls, hour, minute, second, microsecond, tzinfo, *_
+    ):  # type: (int, int, int, int, ...) -> time
+        return time.__new__(cls, hour, minute, second, microsecond, tzinfo)
 
-    def __init__(self, _, trivia, raw):  # type: (time, Trivia, str) -> None
+    def __init__(
+        self, hour, minute, second, microsecond, tzinfo, trivia, raw
+    ):  # type: (int, int, int, int, Trivia, str) -> None
         super(Time, self).__init__(trivia)
 
         self._raw = raw
@@ -627,7 +718,11 @@ class Time(Item, time):
 
     def _getstate(self, protocol=3):
         return (
-            time(self.hour, self.minute, self.second, self.microsecond, self.tzinfo),
+            self.hour,
+            self.minute,
+            self.second,
+            self.microsecond,
+            self.tzinfo,
             self._trivia,
             self._raw,
         )
@@ -809,6 +904,20 @@ class Table(Item, dict):
 
         return self
 
+    def raw_append(self, key, _item):  # type: (Union[Key, str], Any) -> Table
+        if not isinstance(_item, Item):
+            _item = item(_item)
+
+        self._value.append(key, _item)
+
+        if isinstance(key, Key):
+            key = key.key
+
+        if key is not None:
+            super(Table, self).__setitem__(key, _item)
+
+        return self
+
     def remove(self, key):  # type: (Union[Key, str]) -> Table
         self._value.remove(key)
 
@@ -862,6 +971,9 @@ class Table(Item, dict):
         for k, v in other.items():
             self[k] = v
 
+    def get(self, key, default=None):  # type: (Any, Optional[Any]) -> Any
+        return self._value.get(key, default)
+
     def __contains__(self, key):  # type: (Union[Key, str]) -> bool
         return key in self._value
 
@@ -896,6 +1008,9 @@ class Table(Item, dict):
     def __repr__(self):
         return super(Table, self).__repr__()
 
+    def __str__(self):
+        return str(self.value)
+
     def _getstate(self, protocol=3):
         return (
             self._value,
@@ -913,11 +1028,12 @@ class InlineTable(Item, dict):
     """
 
     def __init__(
-        self, value, trivia
-    ):  # type: (tomlkit.container.Container, Trivia) -> None
+        self, value, trivia, new=False
+    ):  # type: (tomlkit.container.Container, Trivia, bool) -> None
         super(InlineTable, self).__init__(trivia)
 
         self._value = value
+        self._new = new
 
         for k, v in self._value.body:
             if k is not None:
@@ -987,6 +1103,8 @@ class InlineTable(Item, dict):
 
             if i != len(self._value.body) - 1:
                 buf += ","
+                if self._new:
+                    buf += " "
 
         buf += "}"
 
@@ -1008,6 +1126,9 @@ class InlineTable(Item, dict):
         for k, v in other.items():
             self[k] = v
 
+    def get(self, key, default=None):  # type: (Any, Optional[Any]) -> Any
+        return self._value.get(key, default)
+
     def __contains__(self, key):  # type: (Union[Key, str]) -> bool
         return key in self._value
 
diff --git a/tomlkit/parser.py b/tomlkit/parser.py
index 7b94833..ecb2a75 100644
--- a/tomlkit/parser.py
+++ b/tomlkit/parser.py
@@ -441,15 +441,21 @@ class Parser:
 
     def _handle_dotted_key(
         self, container, key, value
-    ):  # type: (Container, Key, Any) -> None
+    ):  # type: (Union[Container, Table], Key, Any) -> None
         names = tuple(self._split_table_name(key.key))
         name = names[0]
         name._dotted = True
         if name in container:
-            table = container.item(name)
+            if isinstance(container, Table):
+                table = container.value.item(name)
+            else:
+                table = container.item(name)
         else:
             table = Table(Container(True), Trivia(), False, is_super_table=True)
-            container.append(name, table)
+            if isinstance(container, Table):
+                container.raw_append(name, table)
+            else:
+                container.append(name, table)
 
         for i, _name in enumerate(names[1:]):
             if i == len(names) - 2:
@@ -524,19 +530,41 @@ class Parser:
                 if m.group(1) and m.group(5):
                     # datetime
                     try:
-                        return DateTime(parse_rfc3339(raw), trivia, raw)
+                        dt = parse_rfc3339(raw)
+                        return DateTime(
+                            dt.year,
+                            dt.month,
+                            dt.day,
+                            dt.hour,
+                            dt.minute,
+                            dt.second,
+                            dt.microsecond,
+                            dt.tzinfo,
+                            trivia,
+                            raw,
+                        )
                     except ValueError:
                         raise self.parse_error(InvalidDateTimeError)
 
                 if m.group(1):
                     try:
-                        return Date(parse_rfc3339(raw), trivia, raw)
+                        dt = parse_rfc3339(raw)
+                        return Date(dt.year, dt.month, dt.day, trivia, raw)
                     except ValueError:
                         raise self.parse_error(InvalidDateError)
 
                 if m.group(5):
                     try:
-                        return Time(parse_rfc3339(raw), trivia, raw)
+                        t = parse_rfc3339(raw)
+                        return Time(
+                            t.hour,
+                            t.minute,
+                            t.second,
+                            t.microsecond,
+                            t.tzinfo,
+                            trivia,
+                            raw,
+                        )
                     except ValueError:
                         raise self.parse_error(InvalidTimeError)
 
@@ -918,6 +946,13 @@ class Parser:
         cws, comment, trail = self._parse_comment_trail()
 
         result = Null()
+        table = Table(
+            values,
+            Trivia(indent, cws, comment, trail),
+            is_aot,
+            name=name,
+            display_name=name,
+        )
 
         if len(name_parts) > 1:
             if missing_table:
@@ -967,9 +1002,9 @@ class Parser:
                 _key, item = item
                 if not self._merge_ws(item, values):
                     if _key is not None and _key.is_dotted():
-                        self._handle_dotted_key(values, _key, item)
+                        self._handle_dotted_key(table, _key, item)
                     else:
-                        values.append(_key, item)
+                        table.raw_append(_key, item)
             else:
                 if self._current == "[":
                     is_aot_next, name_next = self._peek_table()
@@ -977,7 +1012,7 @@ class Parser:
                     if self._is_child(name, name_next):
                         key_next, table_next = self._parse_table(name)
 
-                        values.append(key_next, table_next)
+                        table.raw_append(key_next, table_next)
 
                         # Picking up any sibling
                         while not self.end():
@@ -988,7 +1023,7 @@ class Parser:
 
                             key_next, table_next = self._parse_table(name)
 
-                            values.append(key_next, table_next)
+                            table.raw_append(key_next, table_next)
 
                     break
                 else:
@@ -998,13 +1033,7 @@ class Parser:
                     )
 
         if isinstance(result, Null):
-            result = Table(
-                values,
-                Trivia(indent, cws, comment, trail),
-                is_aot,
-                name=name,
-                display_name=name,
-            )
+            result = table
 
             if is_aot and (not self._aot_stack or name != self._aot_stack[-1]):
                 result = self._parse_aot(result, name)
diff --git a/tox.ini b/tox.ini
index ce11ca1..332a28a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 isolated_build=true
-envlist = py27, py34, py35, py36, py37, pypy3
+envlist = py27, py34, py35, py36, py37, py38, pypy3
 
 [testenv]
 whitelist_externals = poetry


Test patch:
diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py
index 1844d11..c68aff7 100644
--- a/tests/test_toml_document.py
+++ b/tests/test_toml_document.py
@@ -7,6 +7,7 @@ import pickle
 
 from datetime import datetime
 
+import tomlkit
 from tomlkit import parse
 from tomlkit._utils import _utc
 
@@ -394,3 +395,50 @@ bar=1"""
     assert doc == {"foo": {"bar": 1}}
     assert doc["foo"]["bar"] == 1
     assert json.loads(json.dumps(doc)) == {"foo": {"bar": 1}}
+
+
+def test_getting_inline_table_is_still_an_inline_table():
+    content = """\
+[tool.poetry]
+name = "foo"
+
+[tool.poetry.dependencies]
+"""
+
+    doc = parse(content)
+    poetry_section = doc["tool"]["poetry"]
+    dependencies = poetry_section["dependencies"]
+    dependencies["foo"] = tomlkit.inline_table()
+    dependencies["foo"]["version"] = "^2.0"
+    dependencies["foo"]["source"] = "local"
+    doc["tool"]["poetry"] = poetry_section
+
+    assert (
+        """\
+[tool.poetry]
+name = "foo"
+
+[tool.poetry.dependencies]
+foo = {version = "^2.0", source = "local"}
+"""
+        == doc.as_string()
+    )
+
+
+def test_declare_sub_table_with_intermediate_table():
+    content = """
+[students]
+tommy = 87
+mary = 66
+
+[subjects]
+maths = "maths"
+english = "english"
+
+[students.bob]
+score = 91
+"""
+
+    doc = parse(content)
+    assert {"tommy": 87, "mary": 66, "bob": {"score": 91}} == doc["students"]
+    assert {"tommy": 87, "mary": 66, "bob": {"score": 91}} == doc.get("students")