From 1e594475d08764392e4d527258c43d0282dc0efc Mon Sep 17 00:00:00 2001 From: Sean O Brien Date: Tue, 24 Oct 2023 17:21:39 +0000 Subject: [PATCH 1/5] Add feature controller. --- .../lambda_runtime_feature_handler.py | 21 +++++++ awslambdaric/lambda_runtime_features.py | 6 ++ awslambdaric/lambda_runtime_marshaller.py | 6 +- requirements/dev.txt | 1 + tests/test_lambda_runtime_features.py | 59 +++++++++++++++++++ tests/test_lambda_runtime_marshaller.py | 4 +- 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 awslambdaric/lambda_runtime_feature_handler.py create mode 100644 awslambdaric/lambda_runtime_features.py create mode 100644 tests/test_lambda_runtime_features.py diff --git a/awslambdaric/lambda_runtime_feature_handler.py b/awslambdaric/lambda_runtime_feature_handler.py new file mode 100644 index 0000000..4d2086b --- /dev/null +++ b/awslambdaric/lambda_runtime_feature_handler.py @@ -0,0 +1,21 @@ +""" +Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. +""" +import os +from .lambda_runtime_features import feature_list + + +class FeatureGate: + def __init__(self, execution_env=os.environ.get("AWS_EXECUTION_ENV")): + self.__execution_env = execution_env + + def is_feature_enabled(self, feature_name) -> bool: + if self.__execution_env is None or self.__execution_env in feature_list.get( + feature_name + ): + return True + else: + return False + + +feature_handler = FeatureGate() diff --git a/awslambdaric/lambda_runtime_features.py b/awslambdaric/lambda_runtime_features.py new file mode 100644 index 0000000..2cbd5eb --- /dev/null +++ b/awslambdaric/lambda_runtime_features.py @@ -0,0 +1,6 @@ +""" +Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. +""" +feature_list = { + "lambda_marshaller_ensure_ascii_false": {"AWS_Lambda_python3.12"}, +} diff --git a/awslambdaric/lambda_runtime_marshaller.py b/awslambdaric/lambda_runtime_marshaller.py index b591e69..7b752fe 100644 --- a/awslambdaric/lambda_runtime_marshaller.py +++ b/awslambdaric/lambda_runtime_marshaller.py @@ -8,6 +8,7 @@ import simplejson as json from .lambda_runtime_exception import FaultException +from .lambda_runtime_feature_handler import feature_handler # simplejson's Decimal encoding allows '-NaN' as an output, which is a parse error for json.loads @@ -15,7 +16,10 @@ # We also set 'ensure_ascii=False' so that the encoded json contains unicode characters instead of unicode escape sequences class Encoder(json.JSONEncoder): def __init__(self): - super().__init__(use_decimal=False, ensure_ascii=False) + if feature_handler.is_feature_enabled("lambda_marshaller_ensure_ascii_false"): + super().__init__(use_decimal=False, ensure_ascii=False) + else: + super().__init__(use_decimal=False) def default(self, obj): if isinstance(obj, decimal.Decimal): diff --git a/requirements/dev.txt b/requirements/dev.txt index c432413..68377ce 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -9,3 +9,4 @@ bandit>=1.6.2 # Test requirements pytest>=3.0.7 mock>=2.0.0 +parameterized>=0.9.0 \ No newline at end of file diff --git a/tests/test_lambda_runtime_features.py b/tests/test_lambda_runtime_features.py new file mode 100644 index 0000000..585295e --- /dev/null +++ b/tests/test_lambda_runtime_features.py @@ -0,0 +1,59 @@ +import os +import unittest +from parameterized import parameterized +from awslambdaric.lambda_runtime_features import feature_list +from awslambdaric.lambda_runtime_feature_handler import FeatureGate + + +feature_names = [(feature_name,) for feature_name in feature_list] +execution_envs = ( + "AWS_Lambda_python3.12", + "AWS_Lambda_python3.11", + "AWS_Lambda_python3.10", + "AWS_Lambda_python3.9", +) +execution_envs_not_enabled_lambda_marshaller_ensure_ascii_false = tuple( + set(execution_envs).difference( + feature_list.get("lambda_marshaller_ensure_ascii_false") + ) +) +execution_envs_is_enabled_lambda_marshaller_ensure_ascii_false = tuple( + feature_list.get("lambda_marshaller_ensure_ascii_false") +) + + +class TestLambdaRuntimeFeatures(unittest.TestCase): + def setUp(self): + self.org_os_environ = os.environ + + def tearDown(self): + os.environ = self.org_os_environ + + @parameterized.expand(feature_names) + def test_no_execution_env_all_features_enabled(self, feature_name): + feature_handler = FeatureGate() + is_feature_enabled = feature_handler.is_feature_enabled(feature_name) + self.assertEqual(is_feature_enabled, True) + + @parameterized.expand( + execution_envs_not_enabled_lambda_marshaller_ensure_ascii_false + ) + def test_not_enabled_lambda_marshaller_ensure_ascii_false(self, execution_env): + os.environ = {"AWS_EXECUTION_ENV": execution_env} + feature_handler = FeatureGate(execution_env=execution_env) + is_feature_enabled = feature_handler.is_feature_enabled( + "lambda_marshaller_ensure_ascii_false" + ) + + self.assertEqual(is_feature_enabled, False) + + @parameterized.expand( + execution_envs_is_enabled_lambda_marshaller_ensure_ascii_false + ) + def test_is_enabled_lambda_marshaller_ensure_ascii_false(self, execution_env): + os.environ = {"AWS_EXECUTION_ENV": execution_env} + feature_handler = FeatureGate(execution_env=execution_env) + is_feature_enabled = feature_handler.is_feature_enabled( + "lambda_marshaller_ensure_ascii_false" + ) + self.assertEqual(is_feature_enabled, True) diff --git a/tests/test_lambda_runtime_marshaller.py b/tests/test_lambda_runtime_marshaller.py index eeb2715..eb8b848 100644 --- a/tests/test_lambda_runtime_marshaller.py +++ b/tests/test_lambda_runtime_marshaller.py @@ -42,4 +42,6 @@ def test_to_json_unicode_encoding(self): response = to_json({"price": "£1.00"}) self.assertEqual('{"price": "£1.00"}', response) self.assertNotEqual('{"price": "\\u00a31.00"}', response) - self.assertEqual(19, len(response.encode('utf-8'))) # would be 23 bytes if a unicode escape was returned + self.assertEqual( + 19, len(response.encode("utf-8")) + ) # would be 23 bytes if a unicode escape was returned From 8077eb1ff187df647dce1053333906cf10524281 Mon Sep 17 00:00:00 2001 From: Sean O Brien Date: Thu, 26 Oct 2023 09:47:18 +0000 Subject: [PATCH 2/5] Refactor tests. --- tests/test_lambda_runtime_features.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_lambda_runtime_features.py b/tests/test_lambda_runtime_features.py index 585295e..b73be9d 100644 --- a/tests/test_lambda_runtime_features.py +++ b/tests/test_lambda_runtime_features.py @@ -23,12 +23,6 @@ class TestLambdaRuntimeFeatures(unittest.TestCase): - def setUp(self): - self.org_os_environ = os.environ - - def tearDown(self): - os.environ = self.org_os_environ - @parameterized.expand(feature_names) def test_no_execution_env_all_features_enabled(self, feature_name): feature_handler = FeatureGate() @@ -39,7 +33,6 @@ def test_no_execution_env_all_features_enabled(self, feature_name): execution_envs_not_enabled_lambda_marshaller_ensure_ascii_false ) def test_not_enabled_lambda_marshaller_ensure_ascii_false(self, execution_env): - os.environ = {"AWS_EXECUTION_ENV": execution_env} feature_handler = FeatureGate(execution_env=execution_env) is_feature_enabled = feature_handler.is_feature_enabled( "lambda_marshaller_ensure_ascii_false" @@ -51,7 +44,6 @@ def test_not_enabled_lambda_marshaller_ensure_ascii_false(self, execution_env): execution_envs_is_enabled_lambda_marshaller_ensure_ascii_false ) def test_is_enabled_lambda_marshaller_ensure_ascii_false(self, execution_env): - os.environ = {"AWS_EXECUTION_ENV": execution_env} feature_handler = FeatureGate(execution_env=execution_env) is_feature_enabled = feature_handler.is_feature_enabled( "lambda_marshaller_ensure_ascii_false" From 09750dfab34ca218f9b3332aab197c12dd6bb6f9 Mon Sep 17 00:00:00 2001 From: Sean O Brien Date: Fri, 27 Oct 2023 12:33:00 +0000 Subject: [PATCH 3/5] Update tests. --- .../lambda_runtime_feature_handler.py | 21 -------- awslambdaric/lambda_runtime_features.py | 6 --- awslambdaric/lambda_runtime_marshaller.py | 5 +- tests/test_lambda_runtime_features.py | 51 ------------------- tests/test_lambda_runtime_marshaller.py | 42 ++++++++++++++- 5 files changed, 42 insertions(+), 83 deletions(-) delete mode 100644 awslambdaric/lambda_runtime_feature_handler.py delete mode 100644 awslambdaric/lambda_runtime_features.py delete mode 100644 tests/test_lambda_runtime_features.py diff --git a/awslambdaric/lambda_runtime_feature_handler.py b/awslambdaric/lambda_runtime_feature_handler.py deleted file mode 100644 index 4d2086b..0000000 --- a/awslambdaric/lambda_runtime_feature_handler.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. -""" -import os -from .lambda_runtime_features import feature_list - - -class FeatureGate: - def __init__(self, execution_env=os.environ.get("AWS_EXECUTION_ENV")): - self.__execution_env = execution_env - - def is_feature_enabled(self, feature_name) -> bool: - if self.__execution_env is None or self.__execution_env in feature_list.get( - feature_name - ): - return True - else: - return False - - -feature_handler = FeatureGate() diff --git a/awslambdaric/lambda_runtime_features.py b/awslambdaric/lambda_runtime_features.py deleted file mode 100644 index 2cbd5eb..0000000 --- a/awslambdaric/lambda_runtime_features.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. -""" -feature_list = { - "lambda_marshaller_ensure_ascii_false": {"AWS_Lambda_python3.12"}, -} diff --git a/awslambdaric/lambda_runtime_marshaller.py b/awslambdaric/lambda_runtime_marshaller.py index 7b752fe..9dde1a0 100644 --- a/awslambdaric/lambda_runtime_marshaller.py +++ b/awslambdaric/lambda_runtime_marshaller.py @@ -4,11 +4,10 @@ import decimal import math - +import os import simplejson as json from .lambda_runtime_exception import FaultException -from .lambda_runtime_feature_handler import feature_handler # simplejson's Decimal encoding allows '-NaN' as an output, which is a parse error for json.loads @@ -16,7 +15,7 @@ # We also set 'ensure_ascii=False' so that the encoded json contains unicode characters instead of unicode escape sequences class Encoder(json.JSONEncoder): def __init__(self): - if feature_handler.is_feature_enabled("lambda_marshaller_ensure_ascii_false"): + if os.environ.get("AWS_EXECUTION_ENV") in {"AWS_Lambda_python3.12"}: super().__init__(use_decimal=False, ensure_ascii=False) else: super().__init__(use_decimal=False) diff --git a/tests/test_lambda_runtime_features.py b/tests/test_lambda_runtime_features.py deleted file mode 100644 index b73be9d..0000000 --- a/tests/test_lambda_runtime_features.py +++ /dev/null @@ -1,51 +0,0 @@ -import os -import unittest -from parameterized import parameterized -from awslambdaric.lambda_runtime_features import feature_list -from awslambdaric.lambda_runtime_feature_handler import FeatureGate - - -feature_names = [(feature_name,) for feature_name in feature_list] -execution_envs = ( - "AWS_Lambda_python3.12", - "AWS_Lambda_python3.11", - "AWS_Lambda_python3.10", - "AWS_Lambda_python3.9", -) -execution_envs_not_enabled_lambda_marshaller_ensure_ascii_false = tuple( - set(execution_envs).difference( - feature_list.get("lambda_marshaller_ensure_ascii_false") - ) -) -execution_envs_is_enabled_lambda_marshaller_ensure_ascii_false = tuple( - feature_list.get("lambda_marshaller_ensure_ascii_false") -) - - -class TestLambdaRuntimeFeatures(unittest.TestCase): - @parameterized.expand(feature_names) - def test_no_execution_env_all_features_enabled(self, feature_name): - feature_handler = FeatureGate() - is_feature_enabled = feature_handler.is_feature_enabled(feature_name) - self.assertEqual(is_feature_enabled, True) - - @parameterized.expand( - execution_envs_not_enabled_lambda_marshaller_ensure_ascii_false - ) - def test_not_enabled_lambda_marshaller_ensure_ascii_false(self, execution_env): - feature_handler = FeatureGate(execution_env=execution_env) - is_feature_enabled = feature_handler.is_feature_enabled( - "lambda_marshaller_ensure_ascii_false" - ) - - self.assertEqual(is_feature_enabled, False) - - @parameterized.expand( - execution_envs_is_enabled_lambda_marshaller_ensure_ascii_false - ) - def test_is_enabled_lambda_marshaller_ensure_ascii_false(self, execution_env): - feature_handler = FeatureGate(execution_env=execution_env) - is_feature_enabled = feature_handler.is_feature_enabled( - "lambda_marshaller_ensure_ascii_false" - ) - self.assertEqual(is_feature_enabled, True) diff --git a/tests/test_lambda_runtime_marshaller.py b/tests/test_lambda_runtime_marshaller.py index eb8b848..d7cb676 100644 --- a/tests/test_lambda_runtime_marshaller.py +++ b/tests/test_lambda_runtime_marshaller.py @@ -4,11 +4,33 @@ import decimal import unittest - +import os +from parameterized import parameterized from awslambdaric.lambda_runtime_marshaller import to_json +execution_envs = ( + "AWS_Lambda_python3.12", + "AWS_Lambda_python3.11", + "AWS_Lambda_python3.10", + "AWS_Lambda_python3.9", +) +envs_is_enabled_lambda_marshaller_ensure_ascii_false = {"AWS_Lambda_python3.12"} + +execution_envs_not_enabled_lambda_marshaller_ensure_ascii_false = tuple( + set(execution_envs).difference(envs_is_enabled_lambda_marshaller_ensure_ascii_false) +) +execution_envs_is_enabled_lambda_marshaller_ensure_ascii_false = tuple( + envs_is_enabled_lambda_marshaller_ensure_ascii_false +) + class TestLambdaRuntimeMarshaller(unittest.TestCase): + def setUp(self): + self.org_os_environ = os.environ + + def tearDown(self): + os.environ = self.org_os_environ + def test_to_json_decimal_encoding(self): response = to_json({"pi": decimal.Decimal("3.14159")}) self.assertEqual('{"pi": 3.14159}', response) @@ -38,10 +60,26 @@ def test_json_serializer_is_not_default_json(self): self.assertFalse(hasattr(stock_json, "YOLO")) self.assertTrue(hasattr(simplejson, "YOLO")) - def test_to_json_unicode_encoding(self): + @parameterized.expand( + execution_envs_is_enabled_lambda_marshaller_ensure_ascii_false + ) + def test_to_json_unicode_not_escaped_encoding(self, execution_env): + os.environ = {"AWS_EXECUTION_ENV": execution_env} response = to_json({"price": "£1.00"}) self.assertEqual('{"price": "£1.00"}', response) self.assertNotEqual('{"price": "\\u00a31.00"}', response) self.assertEqual( 19, len(response.encode("utf-8")) ) # would be 23 bytes if a unicode escape was returned + + @parameterized.expand( + execution_envs_not_enabled_lambda_marshaller_ensure_ascii_false + ) + def test_to_json_unicode_is_escaped_encoding(self, execution_env): + os.environ = {"AWS_EXECUTION_ENV": execution_env} + response = to_json({"price": "£1.00"}) + self.assertEqual('{"price": "\\u00a31.00"}', response) + self.assertNotEqual('{"price": "£1.00"}', response) + self.assertEqual( + 23, len(response.encode("utf-8")) + ) # would be 19 bytes if a escaped was returned From 161b8afcad1ce90bc83e8668ee4d247a322234ad Mon Sep 17 00:00:00 2001 From: Sean O Brien Date: Fri, 27 Oct 2023 16:10:31 +0000 Subject: [PATCH 4/5] Update tests. --- awslambdaric/lambda_runtime_marshaller.py | 2 +- tests/test_lambda_runtime_marshaller.py | 37 +++++++++++------------ 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/awslambdaric/lambda_runtime_marshaller.py b/awslambdaric/lambda_runtime_marshaller.py index 9dde1a0..42ee127 100644 --- a/awslambdaric/lambda_runtime_marshaller.py +++ b/awslambdaric/lambda_runtime_marshaller.py @@ -15,7 +15,7 @@ # We also set 'ensure_ascii=False' so that the encoded json contains unicode characters instead of unicode escape sequences class Encoder(json.JSONEncoder): def __init__(self): - if os.environ.get("AWS_EXECUTION_ENV") in {"AWS_Lambda_python3.12"}: + if os.environ.get("AWS_EXECUTION_ENV") == "AWS_Lambda_python3.12": super().__init__(use_decimal=False, ensure_ascii=False) else: super().__init__(use_decimal=False) diff --git a/tests/test_lambda_runtime_marshaller.py b/tests/test_lambda_runtime_marshaller.py index d7cb676..5e52df0 100644 --- a/tests/test_lambda_runtime_marshaller.py +++ b/tests/test_lambda_runtime_marshaller.py @@ -8,23 +8,24 @@ from parameterized import parameterized from awslambdaric.lambda_runtime_marshaller import to_json -execution_envs = ( - "AWS_Lambda_python3.12", - "AWS_Lambda_python3.11", - "AWS_Lambda_python3.10", - "AWS_Lambda_python3.9", -) -envs_is_enabled_lambda_marshaller_ensure_ascii_false = {"AWS_Lambda_python3.12"} -execution_envs_not_enabled_lambda_marshaller_ensure_ascii_false = tuple( - set(execution_envs).difference(envs_is_enabled_lambda_marshaller_ensure_ascii_false) -) -execution_envs_is_enabled_lambda_marshaller_ensure_ascii_false = tuple( - envs_is_enabled_lambda_marshaller_ensure_ascii_false -) +class TestLambdaRuntimeMarshaller(unittest.TestCase): + execution_envs = ( + "AWS_Lambda_python3.12", + "AWS_Lambda_python3.11", + "AWS_Lambda_python3.10", + "AWS_Lambda_python3.9", + ) + envs_lambda_marshaller_ensure_ascii_false = {"AWS_Lambda_python3.12"} + + execution_envs_lambda_marshaller_ensure_ascii_true = tuple( + set(execution_envs).difference(envs_lambda_marshaller_ensure_ascii_false) + ) + execution_envs_lambda_marshaller_ensure_ascii_false = tuple( + envs_lambda_marshaller_ensure_ascii_false + ) -class TestLambdaRuntimeMarshaller(unittest.TestCase): def setUp(self): self.org_os_environ = os.environ @@ -60,9 +61,7 @@ def test_json_serializer_is_not_default_json(self): self.assertFalse(hasattr(stock_json, "YOLO")) self.assertTrue(hasattr(simplejson, "YOLO")) - @parameterized.expand( - execution_envs_is_enabled_lambda_marshaller_ensure_ascii_false - ) + @parameterized.expand(execution_envs_lambda_marshaller_ensure_ascii_false) def test_to_json_unicode_not_escaped_encoding(self, execution_env): os.environ = {"AWS_EXECUTION_ENV": execution_env} response = to_json({"price": "£1.00"}) @@ -72,9 +71,7 @@ def test_to_json_unicode_not_escaped_encoding(self, execution_env): 19, len(response.encode("utf-8")) ) # would be 23 bytes if a unicode escape was returned - @parameterized.expand( - execution_envs_not_enabled_lambda_marshaller_ensure_ascii_false - ) + @parameterized.expand(execution_envs_lambda_marshaller_ensure_ascii_true) def test_to_json_unicode_is_escaped_encoding(self, execution_env): os.environ = {"AWS_EXECUTION_ENV": execution_env} response = to_json({"price": "£1.00"}) From 7cf12d4d1eef92f5c981fa898b1767b0bd29c08e Mon Sep 17 00:00:00 2001 From: Sean O Brien Date: Fri, 27 Oct 2023 16:18:19 +0000 Subject: [PATCH 5/5] Order imports. --- tests/test_lambda_runtime_marshaller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_lambda_runtime_marshaller.py b/tests/test_lambda_runtime_marshaller.py index 5e52df0..7cd73b4 100644 --- a/tests/test_lambda_runtime_marshaller.py +++ b/tests/test_lambda_runtime_marshaller.py @@ -3,8 +3,8 @@ """ import decimal -import unittest import os +import unittest from parameterized import parameterized from awslambdaric.lambda_runtime_marshaller import to_json