Repository: dropbox/stone

Issue:
python_type_stubs generator should output _validator and route objects.
This was an oversight on my original implementation of `python_type_stubs` - after integrating it into our codebase I've seen a few mypy errors along the lines of 

```
has no attribute 'SomeStoneType_validator
```
and

```
Module has no attribute "name_of_a_route"
```

I might work on it this weekend, just wanted to track as an issue

Discussion:

Patch:
diff --git a/stone/backends/python_type_stubs.py b/stone/backends/python_type_stubs.py
index 6a0c6f0..c087c79 100644
--- a/stone/backends/python_type_stubs.py
+++ b/stone/backends/python_type_stubs.py
@@ -45,6 +45,7 @@ def emit_pass_if_nothing_emitted(codegen):
     ending_lineno = codegen.lineno
     if starting_lineno == ending_lineno:
         codegen.emit("pass")
+        codegen.emit()
 
 class ImportTracker(object):
     def __init__(self):
@@ -135,6 +136,7 @@ class PythonTypeStubsBackend(CodeBackend):
         for alias in namespace.linearize_aliases():
             self._generate_alias_definition(namespace, alias)
 
+        self._generate_routes(namespace)
         self._generate_imports_needed_for_typing()
 
     def _generate_imports_for_referenced_namespaces(self, namespace):
@@ -152,8 +154,17 @@ class PythonTypeStubsBackend(CodeBackend):
         with self.indent():
             self._generate_struct_class_init(ns, data_type)
             self._generate_struct_class_properties(ns, data_type)
+
+        self._generate_validator_for(data_type)
         self.emit()
 
+    def _generate_validator_for(self, data_type):
+        # type: (DataType) -> None
+        cls_name = class_name_for_data_type(data_type)
+        self.emit("{}_validator = ...  # type: stone_validators.Validator".format(
+            cls_name
+        ))
+
     def _generate_union_class(self, ns, data_type):
         # type: (ApiNamespace, Union) -> None
         self.emit(self._class_declaration_for_type(ns, data_type))
@@ -162,6 +173,8 @@ class PythonTypeStubsBackend(CodeBackend):
             self._generate_union_class_is_set(data_type)
             self._generate_union_class_variant_creators(ns, data_type)
             self._generate_union_class_get_helpers(ns, data_type)
+
+        self._generate_validator_for(data_type)
         self.emit()
 
     def _generate_union_class_vars(self, ns, data_type):
@@ -356,6 +369,22 @@ class PythonTypeStubsBackend(CodeBackend):
         return map_stone_type_to_python_type(ns, data_type,
                                              override_dict=self._pep_484_type_mapping_callbacks)
 
+    def _generate_routes(
+            self,
+            namespace,  # type: ApiNamespace
+    ):
+        # type: (...) -> None
+        for route in namespace.routes:
+            var_name = fmt_func(route.name)
+            self.emit(
+                "{var_name} = ...  # type: bb.Route".format(
+                    var_name=var_name
+                )
+            )
+
+        if namespace.routes:
+            self.emit()
+
     def _generate_imports_needed_for_typing(self):
         # type: () -> None
         if self.import_tracker.cur_namespace_typing_imports:
diff --git a/stone/ir/api.py b/stone/ir/api.py
index 2ee40e6..703961e 100644
--- a/stone/ir/api.py
+++ b/stone/ir/api.py
@@ -104,7 +104,7 @@ class ApiNamespace(object):
         self.name = name
         self.doc = None                 # type: typing.Optional[six.text_type]
         self.routes = []                # type: typing.List[ApiRoute]
-        self.route_by_name = {}         # type: typing.Dict[str, ApiRoute]
+        self.route_by_name = {}         # type: typing.Dict[typing.Text, ApiRoute]
         self.data_types = []            # type: typing.List[UserDefined]
         self.data_type_by_name = {}     # type: typing.Dict[str, UserDefined]
         self.aliases = []               # type: typing.List[Alias]
@@ -315,7 +315,7 @@ class ApiRoute(object):
     def __init__(self,
                  name,
                  ast_node):
-        # type: (str, AstRouteDef) -> None
+        # type: (typing.Text, AstRouteDef) -> None
         """
         :param str name: Designated name of the endpoint.
         :param ast_node: Raw route definition from the parser.


Test patch:
diff --git a/test/test_python_type_stubs.py b/test/test_python_type_stubs.py
index dd64bc5..e4423fb 100644
--- a/test/test_python_type_stubs.py
+++ b/test/test_python_type_stubs.py
@@ -1,6 +1,7 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import textwrap
+
 MYPY = False
 if MYPY:
     import typing  # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression
@@ -30,6 +31,7 @@ from stone.ir import (
     UnionField,
     Void,
     Float64)
+from stone.ir.api import ApiRoute
 from stone.backends.python_type_stubs import PythonTypeStubsBackend
 from test.backend_test_util import _mock_emit
 
@@ -167,6 +169,22 @@ def _make_namespace_with_empty_union():
 
     return ns
 
+def _make_namespace_with_route():
+    # type: (...) -> ApiNamespace
+    ns = ApiNamespace("_make_namespace_with_route()")
+    mock_ast_node = Mock()
+    route_one = ApiRoute(
+        name="route_one",
+        ast_node=mock_ast_node,
+    )
+    route_two = ApiRoute(
+        name="route_two",
+        ast_node=mock_ast_node,
+    )
+    ns.add_route(route_one)
+    ns.add_route(route_two)
+    return ns
+
 def _api():
     api = Api(version="1.0")
     return api
@@ -219,7 +237,8 @@ class TestPythonTypeStubs(unittest.TestCase):
 
                 @f1.deleter
                 def f1(self) -> None: ...
-
+            
+            Struct1_validator = ...  # type: stone_validators.Validator
 
             class Struct2(object):
                 def __init__(self,
@@ -256,6 +275,7 @@ class TestPythonTypeStubs(unittest.TestCase):
                 @f4.deleter
                 def f4(self) -> None: ...
 
+            Struct2_validator = ...  # type: stone_validators.Validator
 
 
             from typing import (
@@ -298,6 +318,7 @@ class TestPythonTypeStubs(unittest.TestCase):
                 @nullable_list.deleter
                 def nullable_list(self) -> None: ...
 
+            NestedTypes_validator = ...  # type: stone_validators.Validator
 
 
             from typing import (
@@ -322,6 +343,7 @@ class TestPythonTypeStubs(unittest.TestCase):
 
                 def is_last(self) -> bool: ...
 
+            Union_validator = ...  # type: stone_validators.Validator
 
             class Shape(bb.Union):
                 point = ...  # type: Shape
@@ -335,7 +357,8 @@ class TestPythonTypeStubs(unittest.TestCase):
 
                 def get_circle(self) -> float: ...
 
-
+            Shape_validator = ...  # type: stone_validators.Validator
+            
             """).format(headers=_headers)
         self.assertEqual(result, expected)
 
@@ -348,6 +371,21 @@ class TestPythonTypeStubs(unittest.TestCase):
 
             class EmptyUnion(bb.Union):
                 pass
+                
+            EmptyUnion_validator = ...  # type: stone_validators.Validator
+
+            """).format(headers=_headers)
+        self.assertEqual(result, expected)
+
+    def test__generate_routes(self):
+        # type: () -> None
+        ns = _make_namespace_with_route()
+        result = self._evaluate_namespace(ns)
+        expected = textwrap.dedent("""\
+            {headers}
+
+            route_one = ...  # type: bb.Route
+            route_two = ...  # type: bb.Route
 
             """).format(headers=_headers)
         self.assertEqual(result, expected)
@@ -372,6 +410,7 @@ class TestPythonTypeStubs(unittest.TestCase):
                 @f1.deleter
                 def f1(self) -> None: ...
 
+            Struct1_validator = ...  # type: stone_validators.Validator
 
             AliasToStruct1 = Struct1
             """).format(headers=_headers)