Repository: d0c-s4vage/lookatme (python)

Issue:
Invalid date error from marshmallow validation
**Describe the bug**
I tried setting the `date` metadata field using different formats such as `date: September 2, 2022`, `date: 01/09/2022` yet
kept getting the `marshmallow.exceptions.ValidationError: {'date': ['Not a valid date.']}` when launching `lookatme`.
The reason is that the `date` attribute of the `MetaSchema` requires a `datetime.datetime` instance. Unless I'm misunderstanding the intended use, a valid `date` cannot be set at all via the markdown file.

I changed 

```
    date = fields.Date(
        default=datetime.datetime.now(),
        missing=datetime.datetime.now(),
    )
```

in `lookatme/schemas.py` to

```
    date = fields.Str(
        default="",
        missing="",
    )
```

as for the `title` and `author`. Now I can set the date to any string. I'd be happy to submit a PR for this if you approve.


**To Reproduce**

Steps to reproduce the behavior:

1. Create `md` file
2. Add `date: 01/09/2022` in metadata header
3. Launch `lookatme` pointing to the slides `md file`
4. See error

**Expected behavior**
The `date` string should be printed as I define it, no validation necessary.

**Environment (please complete the following information):**
lookatme, version 2.3.2



Pull request description:
Fixes date parsing error in the schema.
Fixes #147

Specifically
* The date field in the schema is now a string
* Unknown fields at the top level of the schema are allowed
* The styles subschema is strictly validated to help with debugging user settings

Patch:
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 8fe72ab..fba655c 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -74,7 +74,7 @@ A basic, optional YAML header may be included at the top of the slides:
   slides
   dark_theme
   light_theme
-  style_precendence
+  style_precedence
   contrib_extensions_auto
   smart_splitting
   builtin_extensions/index
diff --git a/docs/source/slides.rst b/docs/source/slides.rst
index b81e83f..94eb85d 100644
--- a/docs/source/slides.rst
+++ b/docs/source/slides.rst
@@ -23,6 +23,9 @@ Slide metadata is contained within an optional YAML header:
     styles: {}
     ---
 
+Additional, unknown metadata fields are allowed at the top level. However, the
+``styles`` field and subfields are strictly validated.
+
 Extensions
 ^^^^^^^^^^
 
diff --git a/docs/source/style_precendence.rst b/docs/source/style_precedence.rst
similarity index 100%
rename from docs/source/style_precendence.rst
rename to docs/source/style_precedence.rst
diff --git a/lookatme/schemas.py b/lookatme/schemas.py
index 49210c5..6f04c4c 100644
--- a/lookatme/schemas.py
+++ b/lookatme/schemas.py
@@ -8,7 +8,7 @@ from typing import Dict
 
 import pygments.styles
 import yaml
-from marshmallow import Schema, fields, validate
+from marshmallow import INCLUDE, RAISE, Schema, fields, validate
 
 
 class NoDatesSafeLoader(yaml.SafeLoader):
@@ -168,6 +168,7 @@ class StyleSchema(Schema):
     """
     class Meta:
         render_module = YamlRender
+        unknown = RAISE
 
     style = fields.Str(
         dump_default="monokai",
@@ -224,11 +225,12 @@ class MetaSchema(Schema):
     """
     class Meta:
         render_module = YamlRender
+        unknown = INCLUDE
 
     title = fields.Str(dump_default="", load_default="")
-    date = fields.Date(
-        dump_default=datetime.datetime.now(),
-        load_default=datetime.datetime.now(),
+    date = fields.Str(
+        dump_default=datetime.datetime.now().strftime("%Y-%m-%d"),
+        load_default=datetime.datetime.now().strftime("%Y-%m-%d"),
     )
     author = fields.Str(dump_default="", load_default="")
     styles = fields.Nested(
@@ -242,12 +244,14 @@ class MetaSchema(Schema):
         res = super(self.__class__, self).loads(*args, **kwargs)
         if res is None:
             raise ValueError("Could not loads")
+
         return res
 
     def load(self, *args, **kwargs) -> Dict:
         res = super(self.__class__, self).load(*args, **kwargs)
         if res is None:
             raise ValueError("Could not load")
+
         return res
 
     def dump(self, *args, **kwargs) -> Dict:


Test patch:
diff --git a/tests/test_parse.py b/tests/test_parse.py
index 5206f72..87bd191 100644
--- a/tests/test_parse.py
+++ b/tests/test_parse.py
@@ -13,7 +13,7 @@ def test_parse_metadata():
     presentation
     """
     title = "Presentation Title"
-    date = "9999-01-01"
+    date = "September 2, 2022"
     author = "The Author"
 
     input_data = f"""
@@ -29,9 +29,7 @@ remaining
     input_data, meta = parser.parse_meta(input_data)
     assert input_data.strip() == "remaining"
     assert meta["title"] == title
-    assert meta["date"].year == 9999
-    assert meta["date"].month == 1
-    assert meta["date"].day == 1
+    assert meta["date"] == "September 2, 2022"
     assert meta["author"] == author
 
 
@@ -50,9 +48,7 @@ remaining
     assert input_data.strip() == "remaining"
     now = datetime.datetime.now()
     assert meta["title"] == ""
-    assert meta["date"].year == now.year
-    assert meta["date"].month == now.month
-    assert meta["date"].day == now.day
+    assert meta["date"] == now.strftime("%Y-%m-%d")
     assert meta["author"] == ""
 
 
diff --git a/tests/test_schemas.py b/tests/test_schemas.py
index 274fae7..175c806 100644
--- a/tests/test_schemas.py
+++ b/tests/test_schemas.py
@@ -5,6 +5,7 @@ Test tha lookatme YAML schemas behave as expected
 
 import datetime
 
+import marshmallow.exceptions
 import pytest
 from marshmallow import Schema, fields
 
@@ -26,9 +27,57 @@ date: {date}
     schema = schemas.MetaSchema().loads(yaml_text)
     assert schema["title"] == title
     assert schema["author"] == author
-    assert schema["date"].year == 2019
-    assert schema["date"].month == 1
-    assert schema["date"].day == 1
+    assert schema["date"] == "2019-01-01"
+
+
+def test_meta_schema_allowed_extra_top_level():
+    """Test that extra top-level fields are allowed in the metadata. A separate
+    test will test that the style metadata is strictly validated.
+    """
+    """Test the meta schema
+    """
+    title = "TITLE"
+    author = "AUTHOR"
+    date = "2019-01-01"
+    yaml_text = f"""
+title: {title}
+author: {author}
+date: {date}
+tags: [t1, t2, t3]
+status: status_str
+    """
+
+    schema = schemas.MetaSchema().loads(yaml_text)
+    assert schema["title"] == title
+    assert schema["author"] == author
+    assert schema["date"] == "2019-01-01"
+    assert schema["tags"] == ["t1", "t2", "t3"]
+    assert schema["status"] == "status_str"
+
+
+def test_meta_schema_strict_style_validation():
+    """Test that the style schema is STRICTLY validated. Not having this will
+    make it hard to debug why mistakes in style names aren't having the desired
+    effect on lookatme. We want the user to know what fields they got wrong
+    in the style.
+    """
+    """Test the meta schema
+    """
+    title = "TITLE"
+    author = "AUTHOR"
+    date = "2019-01-01"
+    yaml_text = f"""
+title: {title}
+author: {author}
+date: {date}
+styles:
+  invalid: 100
+    """
+    with pytest.raises(marshmallow.exceptions.ValidationError) as exc_info:
+        schemas.MetaSchema().loads(yaml_text)
+
+    assert exc_info.value.messages_dict \
+        == {"styles": {"invalid": ["Unknown field."]}}
 
 
 def _validate_field_recursive(path, field, gend_value):