Skip to content

Commit

Permalink
Merge pull request #414 from danballance/snake_case_params
Browse files Browse the repository at this point in the history
Fixes #340 | Convert camelcased parameter names to Python style
  • Loading branch information
rafaelcaricio authored Mar 16, 2017
2 parents e9db7fe + cbd6454 commit d1df8f6
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 9 deletions.
12 changes: 10 additions & 2 deletions connexion/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ class Api(object):
def __init__(self, specification, base_url=None, arguments=None,
swagger_json=None, swagger_ui=None, swagger_path=None, swagger_url=None,
validate_responses=False, strict_validation=False, resolver=None,
auth_all_paths=False, debug=False, resolver_error_handler=None, validator_map=None):
auth_all_paths=False, debug=False, resolver_error_handler=None,
validator_map=None, pythonic_params=False):
"""
:type specification: pathlib.Path | dict
:type base_url: str | None
Expand All @@ -81,6 +82,9 @@ def __init__(self, specification, base_url=None, arguments=None,
:param resolver_error_handler: If given, a callable that generates an
Operation used for handling ResolveErrors
:type resolver_error_handler: callable | None
:param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended
to any shadowed built-ins
:type pythonic_params: bool
"""
self.debug = debug
self.validator_map = validator_map
Expand Down Expand Up @@ -142,6 +146,9 @@ def __init__(self, specification, base_url=None, arguments=None,
logger.debug('Strict Request Validation: %s', str(validate_responses))
self.strict_validation = strict_validation

logger.debug('Pythonic params: %s', str(pythonic_params))
self.pythonic_params = pythonic_params

# Create blueprint and endpoints
self.blueprint = self.create_blueprint()

Expand Down Expand Up @@ -186,7 +193,8 @@ def add_operation(self, method, path, swagger_operation, path_parameters):
validate_responses=self.validate_responses,
validator_map=self.validator_map,
strict_validation=self.strict_validation,
resolver=self.resolver)
resolver=self.resolver,
pythonic_params=self.pythonic_params)
self._add_operation_internal(method, path, operation)

def _add_resolver_error_handler(self, method, path, err):
Expand Down
7 changes: 5 additions & 2 deletions connexion/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def common_error_handler(exception):

def add_api(self, specification, base_path=None, arguments=None, auth_all_paths=None, swagger_json=None,
swagger_ui=None, swagger_path=None, swagger_url=None, validate_responses=False,
strict_validation=False, resolver=Resolver(), resolver_error=None):
strict_validation=False, resolver=Resolver(), resolver_error=None, pythonic_params=False):
"""
Adds an API to the application based on a swagger file or API dict
Expand Down Expand Up @@ -129,6 +129,8 @@ def add_api(self, specification, base_path=None, arguments=None, auth_all_paths=
:param resolver_error: If specified, turns ResolverError into error
responses with the given status code.
:type resolver_error: int | None
:param pythonic_params: When True CamelCase parameters are converted to snake_case
:type pythonic_params: bool
:rtype: Api
"""
# Turn the resolver_error code into a handler object
Expand Down Expand Up @@ -165,7 +167,8 @@ def add_api(self, specification, base_path=None, arguments=None, auth_all_paths=
strict_validation=strict_validation,
auth_all_paths=auth_all_paths,
debug=self.debug,
validator_map=self.validator_map)
validator_map=self.validator_map,
pythonic_params=pythonic_params)
self.app.register_blueprint(api.blueprint)
return api

Expand Down
34 changes: 31 additions & 3 deletions connexion/decorators/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
import re

import flask
import inflection
import six
import werkzeug.exceptions as exceptions

from ..utils import all_json, boolean, is_null, is_nullable

try:
import builtins
except ImportError:
import __builtin__ as builtins


logger = logging.getLogger(__name__)

# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#data-types
Expand Down Expand Up @@ -59,11 +66,21 @@ def get_val_from_param(value, query_param):
return make_type(value, query_param["type"])


def sanitize_param(name):
return name and re.sub('^[^a-zA-Z_]+', '', re.sub('[^0-9a-zA-Z_]', '', name))
def snake_and_shadow(name):
"""
Converts the given name into Pythonic form. Firstly it converts CamelCase names to snake_case. Secondly it looks to
see if the name matches a known built-in and if it does it appends an underscore to the name.
:param name: The parameter name
:type name: str
:return:
"""
snake = inflection.underscore(name)
if snake in builtins.__dict__.keys():
return "{}_".format(snake)
return snake


def parameter_to_arg(parameters, consumes, function):
def parameter_to_arg(parameters, consumes, function, pythonic_params=False):
"""
Pass query and body parameters as keyword arguments to handler function.
Expand All @@ -73,8 +90,16 @@ def parameter_to_arg(parameters, consumes, function):
:param consumes: The list of content types the operation consumes
:type consumes: list
:param function: The handler function for the REST endpoint.
:param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended to
any shadowed built-ins
:type pythonic_params: bool
:type function: function|None
"""
def sanitize_param(name):
if name and pythonic_params:
name = snake_and_shadow(name)
return name and re.sub('^[^a-zA-Z_]+', '', re.sub('[^0-9a-zA-Z_]', '', name))

body_parameters = [parameter for parameter in parameters if parameter['in'] == 'body'] or [{}]
body_name = sanitize_param(body_parameters[0].get('name'))
default_body = body_parameters[0].get('schema', {}).get('default')
Expand Down Expand Up @@ -158,6 +183,9 @@ def wrapper(*args, **kwargs):
logger.debug("File parameter (formData) '%s' in function arguments", key)
kwargs[key] = value

# optionally convert parameter variable names to un-shadowed, snake_case form
if pythonic_params:
kwargs = {snake_and_shadow(k): v for k, v in kwargs.items()}
return function(*args, **kwargs)

return wrapper
8 changes: 6 additions & 2 deletions connexion/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def __init__(self, method, path, operation, resolver, app_produces, app_consumes
path_parameters=None, app_security=None, security_definitions=None,
definitions=None, parameter_definitions=None, response_definitions=None,
validate_responses=False, strict_validation=False, randomize_endpoint=None,
validator_map=None):
validator_map=None, pythonic_params=False):
"""
This class uses the OperationID identify the module and function that will handle the operation
Expand Down Expand Up @@ -177,6 +177,9 @@ def __init__(self, method, path, operation, resolver, app_produces, app_consumes
:type validate_responses: bool
:param strict_validation: True enables validation on invalid request parameters
:type strict_validation: bool
:param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended
to any shadowed built-ins
:type pythonic_params: bool
"""

self.method = method
Expand All @@ -196,6 +199,7 @@ def __init__(self, method, path, operation, resolver, app_produces, app_consumes
self.strict_validation = strict_validation
self.operation = operation
self.randomize_endpoint = randomize_endpoint
self.pythonic_params = pythonic_params

# todo support definition references
# todo support references to application level parameters
Expand Down Expand Up @@ -361,7 +365,7 @@ def function(self):
"""

function = parameter_to_arg(
self.parameters, self.consumes, self.__undecorated_function)
self.parameters, self.consumes, self.__undecorated_function, self.pythonic_params)
function = self._request_begin_lifecycle_decorator(function)

if self.validate_responses:
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def read_version(package):
'six>=1.9',
'strict-rfc3339>=0.6',
'swagger-spec-validator>=2.0.2',
'inflection>=0.3.1'
]

if py_major_minor_version < (3, 4):
Expand Down
21 changes: 21 additions & 0 deletions tests/api/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,24 @@ def test_param_sanitization(simple_app):
headers={'Content-Type': 'application/json'})
assert resp.status_code == 200
assert json.loads(resp.data.decode('utf-8', 'replace')) == body


def test_parameters_snake_case(snake_case_app):
app_client = snake_case_app.app.test_client()
headers = {'Content-type': 'application/json'}
resp = app_client.post('/v1.0/test-post-path-snake/123', headers=headers, data=json.dumps({"a": "test"}))
assert resp.status_code == 200
resp = app_client.post('/v1.0/test-post-path-shadow/123', headers=headers, data=json.dumps({"a": "test"}))
assert resp.status_code == 200
resp = app_client.post('/v1.0/test-post-query-snake?someId=123', headers=headers, data=json.dumps({"a": "test"}))
assert resp.status_code == 200
resp = app_client.post('/v1.0/test-post-query-shadow?id=123', headers=headers, data=json.dumps({"a": "test"}))
assert resp.status_code == 200
resp = app_client.get('/v1.0/test-get-path-snake/123')
assert resp.status_code == 200
resp = app_client.get('/v1.0/test-get-path-shadow/123')
assert resp.status_code == 200
resp = app_client.get('/v1.0/test-get-query-snake?someId=123')
assert resp.status_code == 200
resp = app_client.get('/v1.0/test-get-query-shadow?list=123')
assert resp.status_code == 200
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ def simple_app():
return build_app_from_fixture('simple', validate_responses=True)


@pytest.fixture(scope="session")
def snake_case_app():
return build_app_from_fixture('snake_case', validate_responses=True, pythonic_params=True)


@pytest.fixture(scope="session")
def invalid_resp_allowed_app():
return build_app_from_fixture('simple', validate_responses=False)
Expand Down
39 changes: 39 additions & 0 deletions tests/fakeapi/snake_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
def get_path_snake(some_id):
data = {'SomeId': some_id}
return data


def get_path_shadow(id_):
data = {'id': id_}
return data


def get_query_snake(some_id):
data = {'someId': some_id}
return data


def get_query_shadow(list_):
data = {'list': list_}
return data


def post_path_snake(some_id, some_other_id):
data = {'SomeId': some_id, 'SomeOtherId': some_other_id}
return data


def post_path_shadow(id_, round_):
data = {'id': id_, 'reduce': round_}
return data


def post_query_snake(some_id, some_other_id):
data = {'someId': some_id, 'someOtherId': some_other_id}
return data


def post_query_shadow(id_, next_):
data = {'id': id_, 'next': next_}
return data
Loading

0 comments on commit d1df8f6

Please sign in to comment.