Repository: graphql-python/graphene

Issue:
[Enhancement] DateTime inputs should accept Python datetimes
`DateTime`, `Date`, and `Time` will not accept corresponding instances of `datetime.*` when used as variables, you have to format your variables as strings.

The following code simply providing Python datetimes should be supported in addition to string formatted datetimes:
```python
from datetime import date, datetime, time
from graphene import Field, InputObjectType, ObjectType, Schema, String
from graphene.types.datetime import Date, DateTime, Time

class Test(ObjectType):
    date = Date()
    datetime =  DateTime()
    time = Time()


class TestInput(InputObjectType):
    date = Date()
    datetime =  DateTime()
    time = Time()

class Query(ObjectType):
    test = Field(Test, input=TestInput(required=True))

    def resolve_test(self, info, input):
        return Test(**input)

schema = Schema(query=Query)

the_query = """
    query Test($input: TestInput!) {
        test(input: $input) {
            date
            datetime
            time
        }
    }
"""

input = dict(date=date.today(), datetime=datetime.now(), time=time(12, 0))
# currently graphene insists on specifying the input like
# input = dict(date="2018-11-08", datetime="2018-11-08T12:00:00", time="12:00:00")

result = schema.execute(the_query, variable_values=dict(input=input))

print(result.data)
```  

Pull request is on the way.

Comments:

Patch:
diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst
index 21345aa..1c28548 100644
--- a/docs/execution/execute.rst
+++ b/docs/execution/execute.rst
@@ -24,7 +24,7 @@ You can pass context to a query via ``context``.
     class Query(graphene.ObjectType):
         name = graphene.String()
 
-        def resolve_name(self, info):
+        def resolve_name(root, info):
             return info.context.get('name')
 
     schema = graphene.Schema(Query)
@@ -33,7 +33,7 @@ You can pass context to a query via ``context``.
 
 
 Variables
-_______
+_________
 
 You can pass variables to a query via ``variables``.
 
@@ -41,10 +41,10 @@ You can pass variables to a query via ``variables``.
 .. code:: python
 
     class Query(graphene.ObjectType):
-        user = graphene.Field(User)
+        user = graphene.Field(User, id=graphene.ID(required=True))
 
-        def resolve_user(self, info):
-            return info.context.get('user')
+        def resolve_user(root, info, id):
+            return get_user_by_id(id)
 
     schema = graphene.Schema(Query)
     result = schema.execute(
diff --git a/docs/index.rst b/docs/index.rst
index 3e9577a..aff3960 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -19,3 +19,5 @@ Integrations
 * `Graphene-SQLAlchemy <http://docs.graphene-python.org/projects/sqlalchemy/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-sqlalchemy/>`_)
 * `Graphene-GAE <http://docs.graphene-python.org/projects/gae/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-gae/>`_)
 * `Graphene-Mongo <http://graphene-mongo.readthedocs.io/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-mongo>`_)
+* `Starlette <https://www.starlette.io/graphql/>`_ (`source <https://github.com/encode/starlette>`_)
+* `FastAPI <https://fastapi.tiangolo.com/tutorial/graphql/>`_ (`source <https://github.com/tiangolo/fastapi>`_)
diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst
index b6eb308..18f91bd 100644
--- a/docs/types/objecttypes.rst
+++ b/docs/types/objecttypes.rst
@@ -57,8 +57,8 @@ so the first argument to the resolver method ``self`` (or ``root``) need
 not be an actual instance of the ``ObjectType``.
 
 If an explicit resolver is not defined on the ``ObjectType`` then Graphene will
-attempt to use a property with the same name on the object that is passed to the
-``ObjectType``.
+attempt to use a property with the same name on the object or dict that is
+passed to the ``ObjectType``.
 
 .. code:: python
 
@@ -70,54 +70,18 @@ attempt to use a property with the same name on the object that is passed to the
 
     class Query(graphene.ObjectType):
         me = graphene.Field(Person)
+        best_friend = graphene.Field(Person)
 
         def resolve_me(_, info):
             # returns an object that represents a Person
             return get_human(name='Luke Skywalker')
 
-If you are passing a dict instead of an object to your ``ObjectType`` you can
-change the default resolver in the ``Meta`` class like this:
-
-.. code:: python
-
-    import graphene
-    from graphene.types.resolver import dict_resolver
-
-    class Person(graphene.ObjectType):
-        class Meta:
-            default_resolver = dict_resolver
-
-        first_name = graphene.String()
-        last_name = graphene.String()
-
-    class Query(graphene.ObjectType):
-        me = graphene.Field(Person)
-
-        def resolve_me(_, info):
+        def resolve_best_friend(_, info):
             return {
-                "first_name": "Luke",
-                "last_name": "Skywalker",
+                "first_name": "R2",
+                "last_name": "D2",
             }
 
-Or you can change the default resolver globally by calling ``set_default_resolver``
-before executing a query.
-
-.. code:: python
-
-    import graphene
-    from graphene.types.resolver import dict_resolver, set_default_resolver
-
-    set_default_resolver(dict_resolver)
-
-    schema = graphene.Schema(query=Query)
-    result = schema.execute('''
-        query {
-            me {
-                firstName
-            }
-        }
-     ''')
-
 
 Resolvers with arguments
 ~~~~~~~~~~~~~~~~~~~~~~~~
@@ -230,4 +194,17 @@ previous example you could do:
     peter.first_name # prints "Peter"
     peter.last_name # prints "Griffin"
 
+Changing the name
+-----------------
+
+By default the type name in the GraphQL schema will the same as the class name
+that defines the ``ObjectType``. This can be changed by setting the ``name``
+property on the ``Meta`` class:
+
+.. code:: python
+
+    class MyGraphQlSong(graphene.ObjectType):
+        class Meta:
+            name = 'Song'
+
 .. _Interface: /docs/interfaces/
diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py
index 739032b..3519d76 100644
--- a/graphene/types/datetime.py
+++ b/graphene/types/datetime.py
@@ -4,6 +4,7 @@ import datetime
 
 from aniso8601 import parse_date, parse_datetime, parse_time
 from graphql.language import ast
+from six import string_types
 
 from .scalars import Scalar
 
@@ -32,7 +33,10 @@ class Date(Scalar):
     @staticmethod
     def parse_value(value):
         try:
-            return parse_date(value)
+            if isinstance(value, datetime.date):
+                return value
+            elif isinstance(value, string_types):
+                return parse_date(value)
         except ValueError:
             return None
 
@@ -59,7 +63,10 @@ class DateTime(Scalar):
     @staticmethod
     def parse_value(value):
         try:
-            return parse_datetime(value)
+            if isinstance(value, datetime.datetime):
+                return value
+            elif isinstance(value, string_types):
+                return parse_datetime(value)
         except ValueError:
             return None
 
@@ -86,6 +93,9 @@ class Time(Scalar):
     @classmethod
     def parse_value(cls, value):
         try:
-            return parse_time(value)
+            if isinstance(value, datetime.time):
+                return value
+            elif isinstance(value, string_types):
+                return parse_time(value)
         except ValueError:
             return None
diff --git a/graphene/types/resolver.py b/graphene/types/resolver.py
index 888aba8..6a8ea02 100644
--- a/graphene/types/resolver.py
+++ b/graphene/types/resolver.py
@@ -6,7 +6,14 @@ def dict_resolver(attname, default_value, root, info, **args):
     return root.get(attname, default_value)
 
 
-default_resolver = attr_resolver
+def dict_or_attr_resolver(attname, default_value, root, info, **args):
+    resolver = attr_resolver
+    if isinstance(root, dict):
+        resolver = dict_resolver
+    return resolver(attname, default_value, root, info, **args)
+
+
+default_resolver = dict_or_attr_resolver
 
 
 def set_default_resolver(resolver):


Test patch:
diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py
index 98e5e7a..0d9ee11 100644
--- a/graphene/types/tests/test_datetime.py
+++ b/graphene/types/tests/test_datetime.py
@@ -2,6 +2,7 @@ import datetime
 
 import pytz
 from graphql import GraphQLError
+import pytest
 
 from ..datetime import Date, DateTime, Time
 from ..objecttype import ObjectType
@@ -88,6 +89,15 @@ def test_datetime_query_variable():
     now = datetime.datetime.now().replace(tzinfo=pytz.utc)
     isoformat = now.isoformat()
 
+    # test datetime variable provided as Python datetime
+    result = schema.execute(
+        """query Test($date: DateTime){ datetime(in: $date) }""",
+        variables={"date": now},
+    )
+    assert not result.errors
+    assert result.data == {"datetime": isoformat}
+
+    # test datetime variable in string representation
     result = schema.execute(
         """query Test($date: DateTime){ datetime(in: $date) }""",
         variables={"date": isoformat},
@@ -100,6 +110,14 @@ def test_date_query_variable():
     now = datetime.datetime.now().replace(tzinfo=pytz.utc).date()
     isoformat = now.isoformat()
 
+    # test date variable provided as Python date
+    result = schema.execute(
+        """query Test($date: Date){ date(in: $date) }""", variables={"date": now}
+    )
+    assert not result.errors
+    assert result.data == {"date": isoformat}
+
+    # test date variable in string representation
     result = schema.execute(
         """query Test($date: Date){ date(in: $date) }""", variables={"date": isoformat}
     )
@@ -112,8 +130,57 @@ def test_time_query_variable():
     time = datetime.time(now.hour, now.minute, now.second, now.microsecond, now.tzinfo)
     isoformat = time.isoformat()
 
+    # test time variable provided as Python time
+    result = schema.execute(
+        """query Test($time: Time){ time(at: $time) }""", variables={"time": time}
+    )
+    assert not result.errors
+    assert result.data == {"time": isoformat}
+
+    # test time variable in string representation
     result = schema.execute(
         """query Test($time: Time){ time(at: $time) }""", variables={"time": isoformat}
     )
     assert not result.errors
     assert result.data == {"time": isoformat}
+
+
+@pytest.mark.xfail(
+    reason="creating the error message fails when un-parsable object is not JSON serializable."
+)
+def test_bad_variables():
+    def _test_bad_variables(type, input):
+        result = schema.execute(
+            """query Test($input: {}){{ {}(in: $input) }}""".format(type, type.lower()),
+            variables={"input": input},
+        )
+        assert len(result.errors) == 1
+        # when `input` is not JSON serializable formatting the error message in
+        # `graphql.utils.is_valid_value` line 79 fails with a TypeError
+        assert isinstance(result.errors[0], GraphQLError)
+        print(result.errors[0])
+        assert result.data is None
+
+    not_a_date = dict()
+    not_a_date_str = "Some string that's not a date"
+    today = datetime.date.today()
+    now = datetime.datetime.now().replace(tzinfo=pytz.utc)
+    time = datetime.time(now.hour, now.minute, now.second, now.microsecond, now.tzinfo)
+
+    bad_pairs = [
+        ("DateTime", not_a_date),
+        ("DateTime", not_a_date_str),
+        ("DateTime", today),
+        ("DateTime", time),
+        ("Date", not_a_date),
+        ("Date", not_a_date_str),
+        ("Date", now),
+        ("Date", time),
+        ("Time", not_a_date),
+        ("Time", not_a_date_str),
+        ("Time", now),
+        ("Time", today),
+    ]
+
+    for type, input in bad_pairs:
+        _test_bad_variables(type, input)
diff --git a/graphene/types/tests/test_resolver.py b/graphene/types/tests/test_resolver.py
index 2a15028..a03cf18 100644
--- a/graphene/types/tests/test_resolver.py
+++ b/graphene/types/tests/test_resolver.py
@@ -1,6 +1,7 @@
 from ..resolver import (
     attr_resolver,
     dict_resolver,
+    dict_or_attr_resolver,
     get_default_resolver,
     set_default_resolver,
 )
@@ -36,8 +37,16 @@ def test_dict_resolver_default_value():
     assert resolved == "default"
 
 
+def test_dict_or_attr_resolver():
+    resolved = dict_or_attr_resolver("attr", None, demo_dict, info, **args)
+    assert resolved == "value"
+
+    resolved = dict_or_attr_resolver("attr", None, demo_obj, info, **args)
+    assert resolved == "value"
+
+
 def test_get_default_resolver_is_attr_resolver():
-    assert get_default_resolver() == attr_resolver
+    assert get_default_resolver() == dict_or_attr_resolver
 
 
 def test_set_default_resolver_workd():