Skip to content

Commit a5db070

Browse files
torresxb1hawflau
andauthored
fix: Py27hash fix (#2182)
* Add third party py27hash code * Add Py27UniStr and unit tests * Add py27hash_fix utils and tests * Add to_py27_compatible_template and tests * Apply py27hash fix to wherever it is needed * Apply py27hash fix, all tests pass except api_with_any_method_in_swagger * apply py27hash fix in openapi + run black * remove py27 testing * remove other py27 references * black fixes * fixes/typos * remove py27 from tox.ini * refactoring * third party notice * black * Fix py27hash fix to deal with null events * Fix Py27UniStr repr for unicode literals * black reformat * Update _template_has_api_resource to check data type more defensively * Apply py27Dict in _get_authorizers * Apply Py27Dict to authorizers and gateway responses which will go into swagger * Update to_py27_compatible_template to handle parameter_values; Add Py27LongInt class * Rename _convert_to_py27_dict to _convert_to_py27_type * Apply Py27UniStr to path param name * Handle HttpApi resource under to_py27_compatible_template * Fix InvalidDocumentException to not sort different exceptions * black reformat * Remove unnecessary test files Co-authored-by: Wing Fung Lau <[email protected]>
1 parent 8b89575 commit a5db070

34 files changed

+2287
-256
lines changed

DEVELOPMENT_GUIDE.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ Environment Setup
2626
-----------------
2727
### 1. Install Python Versions
2828

29-
Our officially supported Python versions are 2.7, 3.6, 3.7 and 3.8. Follow the idioms from this [excellent cheatsheet](http://python-future.org/compatible_idioms.html) to
30-
make sure your code is compatible with both Python 2.7 and 3 (>=3.6) versions.
31-
Our CI/CD pipeline is setup to run unit tests against both Python 2.7 and 3 versions. So make sure you test it with both versions before sending a Pull Request.
29+
Our officially supported Python versions are 3.6, 3.7 and 3.8.
30+
Our CI/CD pipeline is setup to run unit tests against Python 3 versions. Make sure you test it before sending a Pull Request.
3231
See [Unit testing with multiple Python versions](#unit-testing-with-multiple-python-versions).
3332

3433
[pyenv](https://github.com/pyenv/pyenv) is a great tool to
@@ -41,12 +40,11 @@ easily setup multiple Python versions. For
4140
1. Install PyEnv -
4241
`curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash`
4342
1. Restart shell so the path changes take effect - `exec $SHELL`
44-
1. `pyenv install 2.7.17`
4543
1. `pyenv install 3.6.12`
4644
1. `pyenv install 3.7.9`
4745
1. `pyenv install 3.8.6`
4846
1. Make Python versions available in the project:
49-
`pyenv local 2.7.17 3.6.12 3.7.9 3.8.6`
47+
`pyenv local 3.6.12 3.7.9 3.8.6`
5048

5149
Note: also make sure the following lines were written into your `.bashrc` (or `.zshrc`, depending on which shell you are using):
5250
```
@@ -117,11 +115,10 @@ Running Tests
117115
### Unit testing with one Python version
118116

119117
If you're trying to do a quick run, it's ok to use the current python version. Run `make pr`.
120-
If you're using Python2.7, you can run `make pr2.7` instead.
121118

122119
### Unit testing with multiple Python versions
123120

124-
Currently, our officially supported Python versions are 2.7, 3.6, 3.7 and 3.8. For the most
121+
Currently, our officially supported Python versions are 3.6, 3.7 and 3.8. For the most
125122
part, code that works in Python3.6 will work in Python3.7 and Python3.8. You only run into problems if you are
126123
trying to use features released in a higher version (for example features introduced into Python3.7
127124
will not work in Python3.6). If you want to test in many versions, you can create a virtualenv for

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ recursive-include samtranslator/validator/sam_schema *.json
55
include samtranslator/policy_templates_data/policy_templates.json
66
include samtranslator/policy_templates_data/schema.json
77
include README.md
8+
include THIRD_PARTY_LICENSES
89

910
prune tests

Makefile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ dev: test
2626
# Verifications to run before sending a pull request
2727
pr: black-check init dev
2828

29-
# Verifications to run before sending a pull request, skipping black check because black requires Python 3.6+
30-
pr2.7: init dev
31-
3229
define HELP_MESSAGE
3330

3431
Usage: $ make [TARGETS]

THIRD_PARTY_LICENSES

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
The AWS Serverless Application Model includes the following third-party software/licensing:
2+
3+
** py27hash; version 1.0.2 -- https://pypi.org/project/py27hash/
4+
Copyright (c) 2020 NeuML LLC
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
THE SOFTWARE.
23+
----------------

appveyor-integration-test.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ image: Ubuntu
33

44
environment:
55
matrix:
6-
- TOXENV: py27
7-
PYTHON_VERSION: '2.7'
86
- TOXENV: py36
97
PYTHON_VERSION: '3.6'
108
- TOXENV: py37

appveyor.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ image: Ubuntu
33

44
environment:
55
matrix:
6-
- TOXENV: py27
7-
PYTHON_VERSION: '2.7'
86
- TOXENV: py36
97
PYTHON_VERSION: '3.6'
108
- TOXENV: py37

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.black]
22
line-length = 120
3-
target_version = ['py27', 'py37', 'py36', 'py38']
3+
target_version = ['py37', 'py36', 'py38']
44
exclude = '''
55
66
(

samtranslator/intrinsics/actions.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22

33
from six import string_types
4+
from samtranslator.utils.py27hash_fix import Py27UniStr
45
from samtranslator.model.exceptions import InvalidTemplateException, InvalidDocumentException
56

67

@@ -375,13 +376,13 @@ def handler_method(full_ref, ref_value):
375376

376377
# Find all the pattern, and call the handler to decide how to substitute them.
377378
# Do the substitution and return the final text
378-
return re.sub(
379-
ref_pattern,
380-
# Pass the handler entire string ${logicalId.property} as first parameter and "logicalId.property"
381-
# as second parameter. Return value will be substituted
382-
lambda match: handler_method(match.group(0), match.group(1)),
383-
text,
384-
)
379+
# NOTE: in order to make sure Py27UniStr strings won't be converted to plain string,
380+
# we need to iterate through each match and do the replacement
381+
substituted = text
382+
for match in re.finditer(ref_pattern, text):
383+
sub_value = handler_method(match.group(0), match.group(1))
384+
substituted = substituted.replace(match.group(0), sub_value, 1)
385+
return substituted
385386

386387

387388
class GetAttAction(Action):

samtranslator/model/api/api_generator.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from samtranslator.translator import logical_id_generator
2828
from samtranslator.translator.arn_generator import ArnGenerator
2929
from samtranslator.model.tags.resource_tagging import get_tag_list
30+
from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr
3031

3132
LOG = logging.getLogger(__name__)
3233

@@ -338,7 +339,18 @@ def _construct_body_s3_dict(self):
338339
"'s3://bucket/key' with optional versionId query parameter.",
339340
)
340341

341-
body_s3 = {"Bucket": s3_pointer["Bucket"], "Key": s3_pointer["Key"]}
342+
if isinstance(self.definition_uri, Py27UniStr):
343+
# self.defintion_uri is a Py27UniStr instance if it is defined in the template
344+
# we need to preserve the Py27UniStr type
345+
s3_pointer["Bucket"] = Py27UniStr(s3_pointer["Bucket"])
346+
s3_pointer["Key"] = Py27UniStr(s3_pointer["Key"])
347+
if "Version" in s3_pointer:
348+
s3_pointer["Version"] = Py27UniStr(s3_pointer["Version"])
349+
350+
# Construct body_s3 as py27 dict
351+
body_s3 = Py27Dict()
352+
body_s3["Bucket"] = s3_pointer["Bucket"]
353+
body_s3["Key"] = s3_pointer["Key"]
342354
if "Version" in s3_pointer:
343355
body_s3["Version"] = s3_pointer["Version"]
344356
return body_s3
@@ -922,12 +934,13 @@ def _add_gateway_responses(self):
922934

923935
swagger_editor = SwaggerEditor(self.definition_body)
924936

925-
gateway_responses = {}
937+
# The dicts below will eventually become part of swagger/openapi definition, thus requires using Py27Dict()
938+
gateway_responses = Py27Dict()
926939
for response_type, response in self.gateway_responses.items():
927940
gateway_responses[response_type] = ApiGatewayResponse(
928941
api_logical_id=self.logical_id,
929-
response_parameters=response.get("ResponseParameters", {}),
930-
response_templates=response.get("ResponseTemplates", {}),
942+
response_parameters=response.get("ResponseParameters", Py27Dict()),
943+
response_templates=response.get("ResponseTemplates", Py27Dict()),
931944
status_code=response.get("StatusCode", None),
932945
)
933946

@@ -985,12 +998,12 @@ def _openapi_postprocess(self, definition_body):
985998
SwaggerEditor.get_openapi_version_3_regex(), self.open_api_version
986999
):
9871000
if definition_body.get("securityDefinitions"):
988-
components = definition_body.get("components", {})
1001+
components = definition_body.get("components", Py27Dict())
9891002
components["securitySchemes"] = definition_body["securityDefinitions"]
9901003
definition_body["components"] = components
9911004
del definition_body["securityDefinitions"]
9921005
if definition_body.get("definitions"):
993-
components = definition_body.get("components", {})
1006+
components = definition_body.get("components", Py27Dict())
9941007
components["schemas"] = definition_body["definitions"]
9951008
definition_body["components"] = components
9961009
del definition_body["definitions"]
@@ -1016,15 +1029,17 @@ def _openapi_postprocess(self, definition_body):
10161029
if field_val.get("200") and field_val.get("200").get("headers"):
10171030
headers = field_val["200"]["headers"]
10181031
for header, header_val in headers.items():
1019-
new_header_val_with_schema = {"schema": header_val}
1032+
new_header_val_with_schema = Py27Dict()
1033+
new_header_val_with_schema["schema"] = header_val
10201034
definition_body["paths"][path]["options"][field]["200"]["headers"][
10211035
header
10221036
] = new_header_val_with_schema
10231037

10241038
return definition_body
10251039

10261040
def _get_authorizers(self, authorizers_config, default_authorizer=None):
1027-
authorizers = {}
1041+
# The dict below will eventually become part of swagger/openapi definition, thus requires using Py27Dict()
1042+
authorizers = Py27Dict()
10281043
if default_authorizer == "AWS_IAM":
10291044
authorizers[default_authorizer] = ApiGatewayAuthorizer(
10301045
api_logical_id=self.logical_id, name=default_authorizer, is_aws_iam_authorizer=True

samtranslator/model/apigateway.py

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import json
22
from re import match
3+
from functools import reduce
34
from samtranslator.model import PropertyType, Resource
45
from samtranslator.model.exceptions import InvalidResourceException
56
from samtranslator.model.types import is_type, one_of, is_str, list_of
67
from samtranslator.model.intrinsics import ref, fnSub
78
from samtranslator.translator import logical_id_generator
89
from samtranslator.translator.arn_generator import ArnGenerator
10+
from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr
911

1012

1113
class ApiGatewayRestApi(Resource):
@@ -128,15 +130,16 @@ def __init__(self, api_logical_id=None, response_parameters=None, response_templ
128130
raise InvalidResourceException(api_logical_id, "Property 'StatusCode' must be numeric")
129131

130132
self.api_logical_id = api_logical_id
131-
self.response_parameters = response_parameters or {}
132-
self.response_templates = response_templates or {}
133+
# Defaults to Py27Dict() as these will go into swagger
134+
self.response_parameters = response_parameters or Py27Dict()
135+
self.response_templates = response_templates or Py27Dict()
133136
self.status_code = status_code_str
134137

135138
def generate_swagger(self):
136-
swagger = {
137-
"responseParameters": self._add_prefixes(self.response_parameters),
138-
"responseTemplates": self.response_templates,
139-
}
139+
# Applying Py27Dict here as this goes into swagger
140+
swagger = Py27Dict()
141+
swagger["responseParameters"] = self._add_prefixes(self.response_parameters)
142+
swagger["responseTemplates"] = self.response_templates
140143

141144
# Prevent "null" being written.
142145
if self.status_code:
@@ -146,13 +149,17 @@ def generate_swagger(self):
146149

147150
def _add_prefixes(self, response_parameters):
148151
GATEWAY_RESPONSE_PREFIX = "gatewayresponse."
149-
prefixed_parameters = {}
150-
for key, value in response_parameters.get("Headers", {}).items():
151-
prefixed_parameters[GATEWAY_RESPONSE_PREFIX + "header." + key] = value
152-
for key, value in response_parameters.get("Paths", {}).items():
153-
prefixed_parameters[GATEWAY_RESPONSE_PREFIX + "path." + key] = value
154-
for key, value in response_parameters.get("QueryStrings", {}).items():
155-
prefixed_parameters[GATEWAY_RESPONSE_PREFIX + "querystring." + key] = value
152+
# applying Py27Dict as this is part of swagger
153+
prefixed_parameters = Py27Dict()
154+
155+
parameter_prefix_pairs = [("Headers", "header."), ("Paths", "path."), ("QueryStrings", "querystring.")]
156+
for parameter, prefix in parameter_prefix_pairs:
157+
for key, value in response_parameters.get(parameter, {}).items():
158+
param_key = GATEWAY_RESPONSE_PREFIX + prefix + key
159+
if isinstance(key, Py27UniStr):
160+
# if key is from template, we need to convert param_key to Py27UniStr
161+
param_key = Py27UniStr(param_key)
162+
prefixed_parameters[param_key] = value
156163

157164
return prefixed_parameters
158165

@@ -288,21 +295,20 @@ def _is_missing_identity_source(self, identity):
288295
def generate_swagger(self):
289296
authorizer_type = self._get_type()
290297
APIGATEWAY_AUTHORIZER_KEY = "x-amazon-apigateway-authorizer"
291-
swagger = {
292-
"type": "apiKey",
293-
"name": self._get_swagger_header_name(),
294-
"in": "header",
295-
"x-amazon-apigateway-authtype": self._get_swagger_authtype(),
296-
}
298+
swagger = Py27Dict()
299+
swagger["type"] = "apiKey"
300+
swagger["name"] = self._get_swagger_header_name()
301+
swagger["in"] = "header"
302+
swagger["x-amazon-apigateway-authtype"] = self._get_swagger_authtype()
297303

298304
if authorizer_type == "COGNITO_USER_POOLS":
299-
swagger[APIGATEWAY_AUTHORIZER_KEY] = {
300-
"type": self._get_swagger_authorizer_type(),
301-
"providerARNs": self._get_user_pool_arn_array(),
302-
}
305+
authorizer_dict = Py27Dict()
306+
authorizer_dict["type"] = self._get_swagger_authorizer_type()
307+
authorizer_dict["providerARNs"] = self._get_user_pool_arn_array()
308+
swagger[APIGATEWAY_AUTHORIZER_KEY] = authorizer_dict
303309

304310
elif authorizer_type == "LAMBDA":
305-
swagger[APIGATEWAY_AUTHORIZER_KEY] = {"type": self._get_swagger_authorizer_type()}
311+
swagger[APIGATEWAY_AUTHORIZER_KEY] = Py27Dict({"type": self._get_swagger_authorizer_type()})
306312
partition = ArnGenerator.get_partition_name()
307313
resource = "lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations"
308314
authorizer_uri = fnSub(
@@ -341,35 +347,39 @@ def generate_swagger(self):
341347
def _get_identity_validation_expression(self):
342348
return self.identity and self.identity.get("ValidationExpression")
343349

344-
def _get_identity_source(self):
345-
identity_source_headers = []
346-
identity_source_query_strings = []
347-
identity_source_stage_variables = []
348-
identity_source_context = []
349-
350-
if self.identity.get("Headers"):
351-
identity_source_headers = list(map(lambda h: "method.request.header." + h, self.identity.get("Headers")))
352-
353-
if self.identity.get("QueryStrings"):
354-
identity_source_query_strings = list(
355-
map(lambda qs: "method.request.querystring." + qs, self.identity.get("QueryStrings"))
356-
)
350+
def _build_identity_source_item(self, item_prefix, prop_value):
351+
item = item_prefix + prop_value
352+
if isinstance(prop_value, Py27UniStr):
353+
item = Py27UniStr(item)
354+
return item
357355

358-
if self.identity.get("StageVariables"):
359-
identity_source_stage_variables = list(
360-
map(lambda sv: "stageVariables." + sv, self.identity.get("StageVariables"))
361-
)
362-
363-
if self.identity.get("Context"):
364-
identity_source_context = list(map(lambda c: "context." + c, self.identity.get("Context")))
356+
def _build_identity_source_item_array(self, prop_key, item_prefix):
357+
arr = []
358+
if self.identity.get(prop_key):
359+
arr = [
360+
self._build_identity_source_item(item_prefix, prop_value) for prop_value in self.identity.get(prop_key)
361+
]
362+
return arr
365363

366-
identity_source_array = (
367-
identity_source_headers
368-
+ identity_source_query_strings
369-
+ identity_source_stage_variables
370-
+ identity_source_context
364+
def _get_identity_source(self):
365+
key_prefix_pairs = [
366+
("Headers", "method.request.header."),
367+
("QueryStrings", "method.request.querystring."),
368+
("StageVariables", "stageVariables."),
369+
("Context", "context."),
370+
]
371+
372+
identity_source_array = reduce(
373+
lambda accumulator, key_prefix_pair: accumulator
374+
+ self._build_identity_source_item_array(key_prefix_pair[0], key_prefix_pair[1]),
375+
key_prefix_pairs,
376+
[],
371377
)
378+
372379
identity_source = ", ".join(identity_source_array)
380+
if any(isinstance(i, Py27UniStr) for i in identity_source_array):
381+
# Convert identity_source to Py27UniStr if any part of it is Py27UniStr
382+
identity_source = Py27UniStr(identity_source)
373383

374384
return identity_source
375385

0 commit comments

Comments
 (0)