Repository: Parquery/swagger-to (language: python)

Issue:
Parameter names with '.' in them inconsistently generated into Python code
First off, nice tool: I do some swagger processing for a project of mine so I know the work involved.

So I tried swagger-to to generate Python on a client's swagger for one of their services, and while it ran without complaint, I noticed some of the generated code for a path was inconsistent between the parameter name vs the name used in the code. The problems centered on their use of parameter names that contain a '.' like the following:

```
"parameters": [
  {
    "name": "page.offset",
    "in": "query",
    "description": "Page offset"
    "required": false,
    "type": "string"
  },
  {
    "name": "filter.thing.like",
    "in": "query",
    "description": "wildcard filter",
    "required": false,
    "type": "string"
  }
]
```

The generated method for this path has parameters with names **page_offset** (note the underscore) and **filter_thing_like**. However, these parameters aren't used in the body of the code consistently; instead, the names as specified in the swagger are sometimes used, **page.offset** and **filter.thing.like**. Of course these names don't resolve so these methods don't work.

Here's an snippet from the incorrectly generated code:

```
        if filter.thing.like is not None:
            params['filter.thing.like'] = filter_thing_like
```

It seems the wrong object was referenced when generating the conditional for the if. The name used for the assignment is correct.

I have no idea if using a '.' in a name is legal as far as the swagger spec goes, but they're using it fine so I'm guessing it's ok. It appears that you've made some allowance for this as you transformed the '.' to '_' for the parameter names generated for the method definition. But it seems these transformed names aren't being used consistently in the body of the code. Since you have the proper translation around as evidenced by the parameter names I'm hoping it's a small change to output the correct version of the parameter in the body of the code.


Discussion / hints:
Hi @haxsaw !
Thanks for reporting the issue! I will be able to look into this only in January 2024. If you have some time before, a pull request would be most welcome!

Fix patch:
diff --git a/swagger_to/py_client.py b/swagger_to/py_client.py
index 47fdc2b..70ae240 100644
--- a/swagger_to/py_client.py
+++ b/swagger_to/py_client.py
@@ -1378,7 +1378,7 @@ def {{ function_name }}(
             {% if param.required %}
     {{ set_params_item|trim|indent }}
             {% else %}
-    if {{ param.identifier }} is not None:
+    if {{ param.identifier|arg_name }} is not None:
         {{ set_params_item|trim|indent|indent }}
             {% endif %}{# /if param.required #}
         {% endfor %}{# /for param in request.query_parameters #}


Test patch:
diff --git a/tests/cases/py_client/dots_in_parameters_properties_and_operations/client.py b/tests/cases/py_client/dots_in_parameters_properties_and_operations/client.py
new file mode 100644
index 0000000..d2d08aa
--- /dev/null
+++ b/tests/cases/py_client/dots_in_parameters_properties_and_operations/client.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+# Automatically generated file by swagger_to. DO NOT EDIT OR APPEND ANYTHING!
+"""Implements the client for API."""
+
+# pylint: skip-file
+# pydocstyle: add-ignore=D105,D107,D401
+
+import contextlib
+import json
+from typing import Any, BinaryIO, Dict, List, MutableMapping, Optional, cast
+
+import requests
+import requests.auth
+
+
+def from_obj(obj: Any, expected: List[type], path: str = '') -> Any:
+    """
+    Checks and converts the given obj along the expected types.
+
+    :param obj: to be converted
+    :param expected: list of types representing the (nested) structure
+    :param path: to the object used for debugging
+    :return: the converted object
+    """
+    if not expected:
+        raise ValueError("`expected` is empty, but at least one type needs to be specified.")
+
+    exp = expected[0]
+
+    if exp == float:
+        if isinstance(obj, int):
+            return float(obj)
+
+        if isinstance(obj, float):
+            return obj
+
+        raise ValueError(
+            'Expected object of type int or float at {!r}, but got {}.'.format(path, type(obj)))
+
+    if exp in [bool, int, str, list, dict]:
+        if not isinstance(obj, exp):
+            raise ValueError(
+                'Expected object of type {} at {!r}, but got {}.'.format(exp, path, type(obj)))
+
+    if exp in [bool, int, float, str]:
+        return obj
+
+    if exp == list:
+        lst = []  # type: List[Any]
+        for i, value in enumerate(obj):
+            lst.append(
+                from_obj(value, expected=expected[1:], path='{}[{}]'.format(path, i)))
+
+        return lst
+
+    if exp == dict:
+        adict = dict()  # type: Dict[str, Any]
+        for key, value in obj.items():
+            if not isinstance(key, str):
+                raise ValueError(
+                    'Expected a key of type str at path {!r}, got: {}'.format(path, type(key)))
+
+            adict[key] = from_obj(value, expected=expected[1:], path='{}[{!r}]'.format(path, key))
+
+        return adict
+
+    if exp == SomeDefinition:
+        return some_definition_from_obj(obj, path=path)
+
+    raise ValueError("Unexpected `expected` type: {}".format(exp))
+
+
+def to_jsonable(obj: Any, expected: List[type], path: str = "") -> Any:
+    """
+    Checks and converts the given object along the expected types to a JSON-able representation.
+
+    :param obj: to be converted
+    :param expected: list of types representing the (nested) structure
+    :param path: path to the object used for debugging
+    :return: JSON-able representation of the object
+    """
+    if not expected:
+        raise ValueError("`expected` is empty, but at least one type needs to be specified.")
+
+    exp = expected[0]
+    if not isinstance(obj, exp):
+        raise ValueError('Expected object of type {} at path {!r}, but got {}.'.format(
+            exp, path, type(obj)))
+
+    # Assert on primitive types to help type-hinting.
+    if exp == bool:
+        assert isinstance(obj, bool)
+        return obj
+
+    if exp == int:
+        assert isinstance(obj, int)
+        return obj
+
+    if exp == float:
+        assert isinstance(obj, float)
+        return obj
+
+    if exp == str:
+        assert isinstance(obj, str)
+        return obj
+
+    if exp == list:
+        assert isinstance(obj, list)
+
+        lst = []  # type: List[Any]
+        for i, value in enumerate(obj):
+            lst.append(
+                to_jsonable(value, expected=expected[1:], path='{}[{}]'.format(path, i)))
+
+        return lst
+
+    if exp == dict:
+        assert isinstance(obj, dict)
+
+        adict = dict()  # type: Dict[str, Any]
+        for key, value in obj.items():
+            if not isinstance(key, str):
+                raise ValueError(
+                    'Expected a key of type str at path {!r}, got: {}'.format(path, type(key)))
+
+            adict[key] = to_jsonable(
+                value,
+                expected=expected[1:],
+                path='{}[{!r}]'.format(path, key))
+
+        return adict
+
+    if exp == SomeDefinition:
+        assert isinstance(obj, SomeDefinition)
+        return some_definition_to_jsonable(obj, path=path)
+
+    raise ValueError("Unexpected `expected` type: {}".format(exp))
+
+
+class SomeDefinition:
+    def __init__(
+            self,
+            some_property: Optional[int] = None) -> None:
+        """Initializes with the given values."""
+        self.some_property = some_property
+
+    def to_jsonable(self) -> MutableMapping[str, Any]:
+        """
+        Dispatches the conversion to some_definition_to_jsonable.
+
+        :return: JSON-able representation
+        """
+        return some_definition_to_jsonable(self)
+
+
+def new_some_definition() -> SomeDefinition:
+    """Generates an instance of SomeDefinition with default values."""
+    return SomeDefinition()
+
+
+def some_definition_from_obj(obj: Any, path: str = "") -> SomeDefinition:
+    """
+    Generates an instance of SomeDefinition from a dictionary object.
+
+    :param obj: a JSON-ed dictionary object representing an instance of SomeDefinition
+    :param path: path to the object used for debugging
+    :return: parsed instance of SomeDefinition
+    """
+    if not isinstance(obj, dict):
+        raise ValueError('Expected a dict at path {}, but got: {}'.format(path, type(obj)))
+
+    for key in obj:
+        if not isinstance(key, str):
+            raise ValueError(
+                'Expected a key of type str at path {}, but got: {}'.format(path, type(key)))
+
+    obj_some_property = obj.get('some.property', None)
+    if obj_some_property is not None:
+        some_property_from_obj = from_obj(
+            obj_some_property,
+            expected=[int],
+            path=path + '.some.property')  # type: Optional[int]
+    else:
+        some_property_from_obj = None
+
+    return SomeDefinition(
+        some_property=some_property_from_obj)
+
+
+def some_definition_to_jsonable(
+        some_definition: SomeDefinition,
+        path: str = "") -> MutableMapping[str, Any]:
+    """
+    Generates a JSON-able mapping from an instance of SomeDefinition.
+
+    :param some_definition: instance of SomeDefinition to be JSON-ized
+    :param path: path to the some_definition used for debugging
+    :return: a JSON-able representation
+    """
+    res = dict()  # type: Dict[str, Any]
+
+    if some_definition.some_property is not None:
+        res['some.property'] = some_definition.some_property
+
+    return res
+
+
+class RemoteCaller:
+    """Executes the remote calls to the server."""
+
+    def __init__(
+        self,
+        url_prefix: str,
+        auth: Optional[requests.auth.AuthBase] = None,
+        session: Optional[requests.Session] = None) -> None:
+        self.url_prefix = url_prefix
+        self.auth = auth
+        self.session = session
+
+        if not self.session:
+            self.session = requests.Session()
+            self.session.auth = self.auth
+
+    def do_something(
+            self,
+            some_parameter: 'SomeDefinition',
+            another_parameter: Optional[str] = None) -> MutableMapping[str, Any]:
+        """
+        Send a post request to /do.something.
+
+        :param some_parameter: some test parameter
+        :param another_parameter: another test parameter, this time not required and in query
+
+        :return: Success
+        """
+        url = self.url_prefix + '/do.something'
+
+        params = {}  # type: Dict[str, str]
+
+        if another_parameter is not None:
+            params['another.parameter'] = another_parameter
+
+        data = to_jsonable(
+            some_parameter,
+            expected=[SomeDefinition])
+
+
+        resp = self.session.request(
+            method='post',
+            url=url,
+            params=params,
+            json=data,
+        )
+
+        with contextlib.closing(resp):
+            resp.raise_for_status()
+            return resp.json()
+
+
+# Automatically generated file by swagger_to. DO NOT EDIT OR APPEND ANYTHING!
diff --git a/tests/cases/py_client/dots_in_parameters_properties_and_operations/swagger.yaml b/tests/cases/py_client/dots_in_parameters_properties_and_operations/swagger.yaml
new file mode 100644
index 0000000..f91a224
--- /dev/null
+++ b/tests/cases/py_client/dots_in_parameters_properties_and_operations/swagger.yaml
@@ -0,0 +1,37 @@
+basePath: /
+consumes:
+- application/json
+info:
+  description: An API
+  title: An API
+  version: '1.0'
+definitions:
+  "Some.Definition":
+    properties:
+      "some.property":
+        type: integer
+paths:
+  /do.something:
+    post:
+      operationId: "do.something"
+      parameters:
+        - name: "some.parameter"
+          in: body
+          description: some test parameter
+          required: true
+          schema:
+            $ref: '#/definitions/Some.Definition'
+        - name: "another.parameter"
+          in: query
+          description: another test parameter, this time not required and in query
+          required: false
+          type: string
+      responses:
+        '200':
+          description: Success
+      produces:
+      - application/json
+swagger: '2.0'
+tags:
+  - description: Test API
+    name: API
\ No newline at end of file