Repository: googleapis/gapic-generator-python

Issue:
enum classes are missing docstrings
https://github.com/googleapis/python-bigquery/pull/278#discussion_r497502192

Ideally, there would be a docstring for each enum showing all the available values. This is a regression from the monolith generator.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/googleapis/gapic-generator-python/pull/714?src=pr&el=h1) Report
> Merging [#714](https://codecov.io/gh/googleapis/gapic-generator-python/pull/714?src=pr&el=desc) (99a1bf4) into [master](https://codecov.io/gh/googleapis/gapic-generator-python/commit/edadb228d74630de1595e4151b59ad828ea8fdc6?el=desc) (edadb22) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/googleapis/gapic-generator-python/pull/714/graphs/tree.svg?width=650&height=150&src=pr&token=rvXPSpOilG)](https://codecov.io/gh/googleapis/gapic-generator-python/pull/714?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master      #714   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           26        26           
  Lines         1598      1602    +4     
  Branches       324       326    +2     
=========================================
+ Hits          1598      1602    +4     
```


| [Impacted Files](https://codecov.io/gh/googleapis/gapic-generator-python/pull/714?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [gapic/schema/wrappers.py](https://codecov.io/gh/googleapis/gapic-generator-python/pull/714/diff?src=pr&el=tree#diff-Z2FwaWMvc2NoZW1hL3dyYXBwZXJzLnB5) | `100.00% <ø> (ø)` | |
| [gapic/schema/metadata.py](https://codecov.io/gh/googleapis/gapic-generator-python/pull/714/diff?src=pr&el=tree#diff-Z2FwaWMvc2NoZW1hL21ldGFkYXRhLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/googleapis/gapic-generator-python/pull/714?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/googleapis/gapic-generator-python/pull/714?src=pr&el=footer). Last update [edadb22...99a1bf4](https://codecov.io/gh/googleapis/gapic-generator-python/pull/714?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.gitignore b/.gitignore
index df28dce9..5b68f2ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,9 @@ htmlcov
 # JetBrains
 .idea
 
+# VS Code
+.vscode
+
 # Built documentation
 docs/_build
 docs/_build_doc2dash
diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2
index e3d0016a..4d2f0abe 100644
--- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2
+++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2
@@ -107,7 +107,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
             kwargs: Additional arguments to pass to the constructor.
 
         Returns:
-            {@api.name}: The constructed client.
+            {{ service.client_name }}: The constructed client.
         """
         credentials = service_account.Credentials.from_service_account_info(info)
         kwargs["credentials"] = credentials
@@ -125,7 +125,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
             kwargs: Additional arguments to pass to the constructor.
 
         Returns:
-            {@api.name}: The constructed client.
+            {{ service.client_name }}: The constructed client.
         """
         credentials = service_account.Credentials.from_service_account_file(
             filename)
@@ -188,7 +188,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
             transport (Union[str, ~.{{ service.name }}Transport]): The
                 transport to use. If set to None, a transport is chosen
                 automatically.
-            client_options (client_options_lib.ClientOptions): Custom options for the
+            client_options (google.api_core.client_options.ClientOptions): Custom options for the
                 client. It won't take effect if a ``transport`` instance is provided.
                 (1) The ``api_endpoint`` property can be used to override the
                 default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT
@@ -306,7 +306,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
                 {{ method.input.meta.doc|wrap(width=72, offset=36, indent=16) }}
             {% for key, field in method.flattened_fields.items() -%}
             {{ field.name }} (:class:`{{ field.ident.sphinx }}`):
-                {{ field.meta.doc|rst(width=72, indent=16, nl=False) }}
+                {{ field.meta.doc|rst(width=72, indent=16) }}
                 This corresponds to the ``{{ key }}`` field
                 on the ``request`` instance; if ``request`` is provided, this
                 should not be set.
@@ -329,7 +329,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
             {%- else %}
             Iterable[{{ method.client_output.ident.sphinx }}]:
             {%- endif %}
-                {{ method.client_output.meta.doc|rst(width=72, indent=16) }}
+                {{ method.client_output.meta.doc|rst(width=72, indent=16, source_format='rst') }}
         {%- endif %}
         """
         {%- if not method.client_streaming %}
diff --git a/gapic/ads-templates/docs/%name_%version/types.rst.j2 b/gapic/ads-templates/docs/%name_%version/types.rst.j2
index 640641f2..a46bd12b 100644
--- a/gapic/ads-templates/docs/%name_%version/types.rst.j2
+++ b/gapic/ads-templates/docs/%name_%version/types.rst.j2
@@ -3,3 +3,4 @@ Types for {{ api.naming.long_name }} {{ api.naming.version }} API
 
 .. automodule:: {{ api.naming.namespace|join('.')|lower }}.{{ api.naming.versioned_module_name }}.types
     :members:
+    :undoc-members:
diff --git a/gapic/ads-templates/docs/_static/custom.css b/gapic/ads-templates/docs/_static/custom.css
new file mode 100644
index 00000000..c4e78bac
--- /dev/null
+++ b/gapic/ads-templates/docs/_static/custom.css
@@ -0,0 +1,3 @@
+dl.field-list > dt {
+    min-width: 100px
+}
\ No newline at end of file
diff --git a/gapic/ads-templates/docs/conf.py.j2 b/gapic/ads-templates/docs/conf.py.j2
index 1e827b37..1d60ece9 100644
--- a/gapic/ads-templates/docs/conf.py.j2
+++ b/gapic/ads-templates/docs/conf.py.j2
@@ -165,7 +165,7 @@ html_theme_options = {
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-# html_static_path = []
+html_static_path = ["_static"]
 
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied
diff --git a/gapic/schema/metadata.py b/gapic/schema/metadata.py
index cd96ab86..0276fe09 100644
--- a/gapic/schema/metadata.py
+++ b/gapic/schema/metadata.py
@@ -173,9 +173,22 @@ class Address:
     @property
     def sphinx(self) -> str:
         """Return the Sphinx identifier for this type."""
-        if self.module:
-            return f'~.{self}'
-        return self.name
+
+        if not self.api_naming:
+            if self.package:
+                return '.'.join(self.package + (self.module, self.name))
+            else:
+                return str(self)
+
+        # Check if this is a generated type
+        # Use the original module name rather than the module_alias
+        if self.proto_package.startswith(self.api_naming.proto_package):
+            return '.'.join(self.api_naming.module_namespace + (
+                self.api_naming.versioned_module_name,
+                ) + self.subpackage + ('types',) + self.parent + (self.name, ))
+
+        # Anything left is a standard _pb2 type
+        return f'{self.proto_package}.{self.module}_pb2.{self.name}'
 
     @property
     def subpackage(self) -> Tuple[str, ...]:
diff --git a/gapic/schema/wrappers.py b/gapic/schema/wrappers.py
index 00f22d6d..6020a260 100644
--- a/gapic/schema/wrappers.py
+++ b/gapic/schema/wrappers.py
@@ -690,7 +690,7 @@ class Method:
                 documentation=utils.doc(
                     'An object representing a long-running operation. \n\n'
                     'The result type for the operation will be '
-                    ':class:`{ident}`: {doc}'.format(
+                    ':class:`{ident}` {doc}'.format(
                         doc=self.lro.response_type.meta.doc,
                         ident=self.lro.response_type.ident.sphinx,
                     ),
diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2
index 6354418f..f6e380d0 100644
--- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2
+++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2
@@ -140,7 +140,7 @@ class {{ service.async_client_name }}:
                 {{ method.input.meta.doc|wrap(width=72, offset=36, indent=16) }}
             {% for key, field in method.flattened_fields.items() -%}
             {{ field.name }} (:class:`{{ field.ident.sphinx }}`):
-                {{ field.meta.doc|rst(width=72, indent=16, nl=False) }}
+                {{ field.meta.doc|rst(width=72, indent=16) }}
                 This corresponds to the ``{{ key }}`` field
                 on the ``request`` instance; if ``request`` is provided, this
                 should not be set.
@@ -163,7 +163,7 @@ class {{ service.async_client_name }}:
             {%- else %}
             AsyncIterable[{{ method.client_output_async.ident.sphinx }}]:
             {%- endif %}
-                {{ method.client_output_async.meta.doc|rst(width=72, indent=16) }}
+                {{ method.client_output_async.meta.doc|rst(width=72, indent=16, source_format='rst') }}
         {%- endif %}
         """
         {%- if not method.client_streaming %}
diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2
index 6d703fb2..21b0c9f2 100644
--- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2
+++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2
@@ -123,7 +123,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
             kwargs: Additional arguments to pass to the constructor.
 
         Returns:
-            {@api.name}: The constructed client.
+            {{ service.client_name }}: The constructed client.
         """
         credentials = service_account.Credentials.from_service_account_info(info)
         kwargs["credentials"] = credentials
@@ -141,7 +141,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
             kwargs: Additional arguments to pass to the constructor.
 
         Returns:
-            {@api.name}: The constructed client.
+            {{ service.client_name }}: The constructed client.
         """
         credentials = service_account.Credentials.from_service_account_file(
             filename)
@@ -202,10 +202,10 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
                 credentials identify the application to the service; if none
                 are specified, the client will attempt to ascertain the
                 credentials from the environment.
-            transport (Union[str, ~.{{ service.name }}Transport]): The
+            transport (Union[str, {{ service.name }}Transport]): The
                 transport to use. If set to None, a transport is chosen
                 automatically.
-            client_options (client_options_lib.ClientOptions): Custom options for the
+            client_options (google.api_core.client_options.ClientOptions): Custom options for the
                 client. It won't take effect if a ``transport`` instance is provided.
                 (1) The ``api_endpoint`` property can be used to override the
                 default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT
@@ -322,18 +322,18 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
 
         Args:
             {%- if not method.client_streaming %}
-            request (:class:`{{ method.input.ident.sphinx }}`):
+            request ({{ method.input.ident.sphinx }}):
                 The request object.{{ ' ' -}}
                 {{ method.input.meta.doc|wrap(width=72, offset=36, indent=16) }}
             {% for key, field in method.flattened_fields.items() -%}
-            {{ field.name }} (:class:`{{ field.ident.sphinx }}`):
-                {{ field.meta.doc|rst(width=72, indent=16, nl=False) }}
+            {{ field.name }} ({{ field.ident.sphinx }}):
+                {{ field.meta.doc|rst(width=72, indent=16) }}
                 This corresponds to the ``{{ key }}`` field
                 on the ``request`` instance; if ``request`` is provided, this
                 should not be set.
             {% endfor -%}
             {%- else %}
-            requests (Iterator[`{{ method.input.ident.sphinx }}`]):
+            requests (Iterator[{{ method.input.ident.sphinx }}]):
                 The request object iterator.{{ ' ' -}}
                 {{ method.input.meta.doc|wrap(width=72, offset=36, indent=16) }}
             {%- endif %}
@@ -350,7 +350,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
             {%- else %}
             Iterable[{{ method.client_output.ident.sphinx }}]:
             {%- endif %}
-                {{ method.client_output.meta.doc|rst(width=72, indent=16) }}
+                {{ method.client_output.meta.doc|rst(width=72, indent=16, source_format='rst') }}
         {%- endif %}
         """
         {%- if not method.client_streaming %}
diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/pagers.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/pagers.py.j2
index 2f9598d2..ea08466b 100644
--- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/pagers.py.j2
+++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/pagers.py.j2
@@ -45,9 +45,9 @@ class {{ method.name }}Pager:
         Args:
             method (Callable): The method that was originally called, and
                 which instantiated this pager.
-            request (:class:`{{ method.input.ident.sphinx }}`):
+            request ({{ method.input.ident.sphinx }}):
                 The initial request object.
-            response (:class:`{{ method.output.ident.sphinx }}`):
+            response ({{ method.output.ident.sphinx }}):
                 The initial response object.
             metadata (Sequence[Tuple[str, str]]): Strings which should be
                 sent along with the request as metadata.
@@ -104,9 +104,9 @@ class {{ method.name }}AsyncPager:
         Args:
             method (Callable): The method that was originally called, and
                 which instantiated this pager.
-            request (:class:`{{ method.input.ident.sphinx }}`):
+            request ({{ method.input.ident.sphinx }}):
                 The initial request object.
-            response (:class:`{{ method.output.ident.sphinx }}`):
+            response ({{ method.output.ident.sphinx }}):
                 The initial response object.
             metadata (Sequence[Tuple[str, str]]): Strings which should be
                 sent along with the request as metadata.
diff --git a/gapic/templates/docs/%name_%version/%service.rst.j2 b/gapic/templates/docs/%name_%version/%service.rst.j2
new file mode 100644
index 00000000..6a54833b
--- /dev/null
+++ b/gapic/templates/docs/%name_%version/%service.rst.j2
@@ -0,0 +1,12 @@
+{{ service.name }}
+{{ '-' * (18 + service.name|length) }}
+
+.. automodule:: {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}
+    :members:
+    :inherited-members:
+
+{% if service.has_pagers %}
+.. automodule:: {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.pagers
+    :members:
+    :inherited-members:
+{% endif %}
\ No newline at end of file
diff --git a/gapic/templates/docs/%name_%version/services.rst.j2 b/gapic/templates/docs/%name_%version/services.rst.j2
index b0f05d69..98ba64f6 100644
--- a/gapic/templates/docs/%name_%version/services.rst.j2
+++ b/gapic/templates/docs/%name_%version/services.rst.j2
@@ -1,8 +1,8 @@
 Services for {{ api.naming.long_name }} {{ api.naming.version }} API
 {{ '=' * (18 + api.naming.long_name|length + api.naming.version|length) }}
+.. toctree::
+    :maxdepth: 2
 
-{% for service in api.services.values()|sort(attribute='name') -%}
-.. automodule:: {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}
-    :members:
-    :inherited-members:
-{% endfor %}
+    {% for service in api.services.values()|sort(attribute='name') -%}
+    {{ service.name|snake_case }}
+    {% endfor %}
diff --git a/gapic/templates/docs/%name_%version/types.rst.j2 b/gapic/templates/docs/%name_%version/types.rst.j2
index a77df003..2f453dd8 100644
--- a/gapic/templates/docs/%name_%version/types.rst.j2
+++ b/gapic/templates/docs/%name_%version/types.rst.j2
@@ -3,4 +3,5 @@ Types for {{ api.naming.long_name }} {{ api.naming.version }} API
 
 .. automodule:: {{ api.naming.namespace|join('.')|lower }}.{{ api.naming.versioned_module_name }}.types
     :members:
+    :undoc-members:
     :show-inheritance:
diff --git a/gapic/templates/docs/_static/custom.css b/gapic/templates/docs/_static/custom.css
new file mode 100644
index 00000000..c4e78bac
--- /dev/null
+++ b/gapic/templates/docs/_static/custom.css
@@ -0,0 +1,3 @@
+dl.field-list > dt {
+    min-width: 100px
+}
\ No newline at end of file
diff --git a/gapic/templates/docs/conf.py.j2 b/gapic/templates/docs/conf.py.j2
index 423c3ad0..d4f1b215 100644
--- a/gapic/templates/docs/conf.py.j2
+++ b/gapic/templates/docs/conf.py.j2
@@ -165,7 +165,7 @@ html_theme_options = {
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-# html_static_path = []
+html_static_path = ["_static"]
 
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied
@@ -347,6 +347,7 @@ intersphinx_mapping = {
     "grpc": ("https://grpc.io/grpc/python/", None),
     "requests": ("http://requests.kennethreitz.org/en/stable/", None),
     "proto": ("https://proto-plus-python.readthedocs.io/en/stable", None),
+    "protobuf": ("https://googleapis.dev/python/protobuf/latest/", None),
 }
 
 
diff --git a/gapic/templates/noxfile.py.j2 b/gapic/templates/noxfile.py.j2
index 5fde488f..65397ea7 100644
--- a/gapic/templates/noxfile.py.j2
+++ b/gapic/templates/noxfile.py.j2
@@ -2,6 +2,7 @@
 
 {% block content %}
 import os
+import shutil
 
 import nox  # type: ignore
 
@@ -37,4 +38,25 @@ def mypy(session):
         '{{ api.naming.versioned_module_name }}',
         {%- endif %}
     )
+
+@nox.session(python='3.6')
+def docs(session):
+    """Build the docs for this library."""
+
+    session.install("-e", ".")
+    session.install("sphinx<3.0.0", "alabaster", "recommonmark")
+
+    shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True)
+    session.run(
+        "sphinx-build",
+        "-W",  # warnings as errors
+        "-T",  # show full traceback on exception
+        "-N",  # no colors
+        "-b",
+        "html",
+        "-d",
+        os.path.join("docs", "_build", "doctrees", ""),
+        os.path.join("docs", ""),
+        os.path.join("docs", "_build", "html", ""),
+    )
 {% endblock %}


Test patch:
diff --git a/tests/unit/schema/wrappers/test_enums.py b/tests/unit/schema/wrappers/test_enums.py
index 0602a09e..3debb560 100644
--- a/tests/unit/schema/wrappers/test_enums.py
+++ b/tests/unit/schema/wrappers/test_enums.py
@@ -39,4 +39,4 @@ def test_enum_value_properties():
 def test_enum_ident():
     message = make_enum('Baz', package='foo.v1', module='bar')
     assert str(message.ident) == 'bar.Baz'
-    assert message.ident.sphinx == '~.bar.Baz'
+    assert message.ident.sphinx == 'foo.v1.bar.Baz'
diff --git a/tests/unit/schema/wrappers/test_message.py b/tests/unit/schema/wrappers/test_message.py
index 7d8cca16..4a7905d1 100644
--- a/tests/unit/schema/wrappers/test_message.py
+++ b/tests/unit/schema/wrappers/test_message.py
@@ -21,6 +21,7 @@ import pytest
 from google.api import resource_pb2
 from google.protobuf import descriptor_pb2
 
+from gapic.schema import naming
 from gapic.schema import metadata
 from gapic.schema import wrappers
 
@@ -50,7 +51,7 @@ def test_message_docstring():
 def test_message_ident():
     message = make_message('Baz', package='foo.v1', module='bar')
     assert str(message.ident) == 'bar.Baz'
-    assert message.ident.sphinx == '~.bar.Baz'
+    assert message.ident.sphinx == 'foo.v1.bar.Baz'
 
 
 def test_message_ident_collisions():
@@ -58,7 +59,23 @@ def test_message_ident_collisions():
         collisions={'bar'},
     )
     assert str(message.ident) == 'fv_bar.Baz'
-    assert message.ident.sphinx == '~.fv_bar.Baz'
+    assert message.ident.sphinx == 'foo.v1.bar.Baz'
+
+
+def test_message_pb2_sphinx_ident():
+    meta = metadata.Metadata(
+        address=metadata.Address(
+            name='Timestamp',
+            package=('google', 'protobuf'),
+            module='timestamp',
+            api_naming=naming.NewNaming(
+                proto_package="foo.bar"
+            )
+        )
+    )
+    message = make_message("Timestamp", package='google.protobuf',
+        module='timestamp', meta=meta)
+    assert message.ident.sphinx == 'google.protobuf.timestamp_pb2.Timestamp'
 
 
 def test_get_field():