18874: Merge commit '6f8dcb2b13f3058db656908fb26b09e23b527f08' into 18874-merge-wb2
[arvados.git] / sdk / python / discovery2pydoc.py
index 4f74d8836bb6ae1d89f3c8f5292be7bb86ac9304..9f7f87d988e64534f196ff087a366be21dd6e9ff 100755 (executable)
@@ -68,6 +68,41 @@ _DEPRECATED_SCHEMAS = frozenset([
     *(f'{name[:-1]}List' for name in _DEPRECATED_RESOURCES),
 ])
 
+_LIST_PYDOC = '''
+
+This is the dictionary object returned when you call `{cls_name}s.list`.
+If you just want to iterate all objects that match your search criteria,
+consider using `arvados.util.keyset_list_all`.
+If you work with this raw object, the keys of the dictionary are documented
+below, along with their types. The `items` key maps to a list of matching
+`{cls_name}` objects.
+'''
+_MODULE_PYDOC = '''Arvados API client documentation skeleton
+
+This module documents the methods and return types provided by the Arvados API
+client. Start with `ArvadosAPIClient`, which documents the methods available
+from the API client objects constructed by `arvados.api`. The implementation is
+generated dynamically at runtime when the client object is built.
+'''
+_SCHEMA_PYDOC = '''
+
+This is the dictionary object that represents a single {cls_name} in Arvados
+and is returned by most `{cls_name}s` methods.
+The keys of the dictionary are documented below, along with their types.
+Not every key may appear in every dictionary returned by an API call.
+When a method doesn't return all the data, you can use its `select` parameter
+to list the specific keys you need. Refer to the API documentation for details.
+'''
+
+_MODULE_PRELUDE = '''
+import sys
+if sys.version_info < (3, 8):
+    from typing import Any
+    from typing_extensions import TypedDict
+else:
+    from typing import Any, TypedDict
+'''
+
 _TYPE_MAP = {
     # Map the API's JavaScript-based type names to Python annotations.
     # Some of these may disappear after Arvados issue #19795 is fixed.
@@ -175,7 +210,7 @@ class Method:
 
     def signature(self) -> inspect.Signature:
         parameters = [
-            inspect.Parameter('self', inspect.Parameter.POSITIONAL_ONLY),
+            inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
             *self._required_params,
             *self._optional_params,
         ]
@@ -205,6 +240,13 @@ def document_schema(name: str, spec: Mapping[str, Any]) -> str:
     description = spec['description']
     if name in _DEPRECATED_SCHEMAS:
         description += _DEPRECATED_NOTICE
+    if name.endswith('List'):
+        desc_fmt = _LIST_PYDOC
+        cls_name = name[:-4]
+    else:
+        desc_fmt = _SCHEMA_PYDOC
+        cls_name = name
+    description += desc_fmt.format(cls_name=cls_name)
     lines = [
         f"class {name}(TypedDict, total=False):",
         to_docstring(description, 4),
@@ -276,23 +318,29 @@ If not provided, retrieved dynamically from Arvados client configuration.
         parts = urllib.parse.urlsplit(args.discovery_url)
         if not (parts.scheme or parts.netloc):
             args.discovery_url = pathlib.Path(args.discovery_url).resolve().as_uri()
+    # Our output is Python source, so it should be UTF-8 regardless of locale.
     if args.output_file == STDSTREAM_PATH:
-        args.out_file = sys.stdout
+        args.out_file = open(sys.stdout.fileno(), 'w', encoding='utf-8', closefd=False)
     else:
-        args.out_file = args.output_file.open('w')
+        args.out_file = args.output_file.open('w', encoding='utf-8')
     return args
 
 def main(arglist: Optional[Sequence[str]]=None) -> int:
     args = parse_arguments(arglist)
     with urllib.request.urlopen(args.discovery_url) as discovery_file:
-        if not (discovery_file.status is None or 200 <= discovery_file.status < 300):
+        status = discovery_file.getcode()
+        if not (status is None or 200 <= status < 300):
             print(
                 f"error getting {args.discovery_url}: server returned {discovery_file.status}",
                 file=sys.stderr,
             )
             return os.EX_IOERR
         discovery_document = json.load(discovery_file)
-    print('''from typing import Any, TypedDict''', file=args.out_file)
+    print(
+        to_docstring(_MODULE_PYDOC, indent=0),
+        _MODULE_PRELUDE,
+        sep='\n', file=args.out_file,
+    )
 
     schemas = sorted(discovery_document['schemas'].items())
     for name, schema_spec in schemas:
@@ -317,6 +365,7 @@ def main(arglist: Optional[Sequence[str]]=None) -> int:
         }
         print(Method(name, method_spec).doc(), file=args.out_file)
 
+    args.out_file.close()
     return os.EX_OK
 
 if __name__ == '__main__':