From 1d773903bc170d188e4370917b0d41489eb6862e Mon Sep 17 00:00:00 2001 From: Ahmed TAHRI Date: Thu, 21 May 2020 01:37:13 +0200 Subject: [PATCH 1/8] :zap: No need to rebuild the content when iadd is called --- kiss_headers/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kiss_headers/models.py b/kiss_headers/models.py index 73f37dd..b0fddd4 100644 --- a/kiss_headers/models.py +++ b/kiss_headers/models.py @@ -196,7 +196,8 @@ def __iadd__(self, other: Union[str, "Header"]) -> "Header": ) self._attrs.insert(other, None) - self._content = str(self._attrs) + # No need to rebuild the content completely. + self._content += "; " + other if self._content.lstrip() != "" else other return self From 77af0ba069907848a44fb6f7b42290ffd15f4158 Mon Sep 17 00:00:00 2001 From: Ahmed TAHRI Date: Thu, 21 May 2020 01:40:12 +0200 Subject: [PATCH 2/8] :bug: Class _members of the Header class wasn't refreshed after mod --- kiss_headers/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kiss_headers/models.py b/kiss_headers/models.py index b0fddd4..b51499e 100644 --- a/kiss_headers/models.py +++ b/kiss_headers/models.py @@ -170,6 +170,8 @@ def insert( __index += 1 self._content = str(self._attrs) + # We need to update our list of members + self._members = header_content_split(self._content, ";") def __iadd__(self, other: Union[str, "Header"]) -> "Header": """ @@ -198,6 +200,7 @@ def __iadd__(self, other: Union[str, "Header"]) -> "Header": self._attrs.insert(other, None) # No need to rebuild the content completely. self._content += "; " + other if self._content.lstrip() != "" else other + self._members.append(other) return self @@ -257,6 +260,7 @@ def __isub__(self, other: str) -> "Header": self._attrs.remove(other) self._content = str(self._attrs) + self._members = header_content_split(self._content, ";") return self @@ -305,6 +309,7 @@ def __setitem__(self, key: str, value: str) -> None: self._attrs.insert(key, value) self._content = str(self._attrs) + self._members = header_content_split(self._content, ";") def __delitem__(self, key: str) -> None: """ @@ -325,6 +330,7 @@ def __delitem__(self, key: str) -> None: self._attrs.remove(key) self._content = str(self._attrs) + self._members = header_content_split(self._content, ";") def __delattr__(self, item: str) -> None: """ From 7c32b534238f193b4c75e048e90dcc8b28509c29 Mon Sep 17 00:00:00 2001 From: Ahmed TAHRI Date: Thu, 21 May 2020 01:40:31 +0200 Subject: [PATCH 3/8] :pencil: Minor README.md update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ff5e4d..7a5b6d9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Welcome to Headers for Humans 👋

+

Welcome to Headers for Humans 👋

Object oriented headers, parser and builder.
From 20007869aa793ca7874ad52be9278160f8eff3f9 Mon Sep 17 00:00:00 2001 From: Ahmed TAHRI Date: Fri, 22 May 2020 00:36:42 +0200 Subject: [PATCH 4/8] :bug: Fix regression seen in 2.2.0, access attrs case insensitive. --- kiss_headers/models.py | 27 +++++++++++++-------------- kiss_headers/utils.py | 5 +++++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/kiss_headers/models.py b/kiss_headers/models.py index b51499e..e9ce7bf 100644 --- a/kiss_headers/models.py +++ b/kiss_headers/models.py @@ -13,6 +13,7 @@ unfold, unpack_protected_keyword, unquote, + normalize_list, ) OUTPUT_LOCK_TYPE: bool = False @@ -321,9 +322,9 @@ def __delitem__(self, key: str) -> None: >>> str(headers.content_type) 'text/html' """ - if key not in self._attrs: + if key not in normalize_list(self.valued_attrs): raise KeyError( - "'{item}' attribute is not defined within '{header}' header.".format( + "'{item}' attribute is not defined or have at least one value associated within '{header}' header.".format( item=key, header=self.name ) ) @@ -344,9 +345,9 @@ def __delattr__(self, item: str) -> None: """ item = normalize_str(item) - if item not in self._attrs: + if item not in normalize_list(self.valued_attrs): raise AttributeError( - "'{item}' attribute is not defined within '{header}' header.".format( + "'{item}' attribute is not defined or have at least one value associated within '{header}' header.".format( item=item, header=self.name ) ) @@ -402,14 +403,12 @@ def __dir__(self) -> Iterable[str]: Provide a better auto-completion when using a Python interpreter. We are feeding __dir__ so Python can be aware of what properties are callable. In other words, more precise auto-completion when not using IDE. """ - return list(super().__dir__()) + [ - normalize_str(key) for key in self._attrs.keys() - ] + return list(super().__dir__()) + normalize_list(self._attrs.keys()) @property def attrs(self) -> List[str]: """ - List of members or attributes found in provided content. This list is ordered. + List of members or attributes found in provided content. This list is ordered and normalized. eg. Content-Type: application/json; charset=utf-8; format=origin Would output : ['application/json', 'charset', 'format'] """ @@ -427,7 +426,7 @@ def attrs(self) -> List[str]: @property def valued_attrs(self) -> List[str]: """ - List of distinct attributes that have at least one value associated with them. This list is ordered. + List of distinct attributes that have at least one value associated with them. This list is ordered and normalized. This property could have been written under the keys() method, but implementing it would interfere with dict() cast and the __iter__() method. eg. Content-Type: application/json; charset=utf-8; format=origin @@ -465,7 +464,7 @@ def get(self, attr: str) -> Optional[Union[str, List[str]]]: >>> header.format 'flowed' """ - if attr not in self._attrs: + if normalize_str(attr) not in normalize_list(self.valued_attrs): return None return self._attrs[attr] # type: ignore @@ -495,11 +494,11 @@ def __getitem__(self, item: Union[str, int]) -> Union[str, List[str]]: self._members[item] if not OUTPUT_LOCK_TYPE else [self._members[item]] ) - if item in self._attrs: + if normalize_str(item) in normalize_list(self.valued_attrs): value = self._attrs[item] # type: ignore else: raise KeyError( - "'{item}' attribute is not defined within '{header}' header.".format( + "'{item}' attribute is not defined or does not have at least one value within the '{header}' header.".format( item=item, header=self.name ) ) @@ -520,9 +519,9 @@ def __getattr__(self, item: str) -> Union[str, List[str]]: """ item = unpack_protected_keyword(item) - if item not in self._attrs: + if normalize_str(item) not in normalize_list(self.valued_attrs): raise AttributeError( - "'{item}' attribute is not defined within '{header}' header.".format( + "'{item}' attribute is not defined or have at least one value within '{header}' header.".format( item=item, header=self.name ) ) diff --git a/kiss_headers/utils.py b/kiss_headers/utils.py index 16e2969..d35e809 100644 --- a/kiss_headers/utils.py +++ b/kiss_headers/utils.py @@ -34,6 +34,11 @@ def normalize_str(string: str) -> str: return string.lower().replace("-", "_") +def normalize_list(strings: List[str]) -> List[str]: + """Normalize a list of string by applying fn normalize_str over each element.""" + return list(map(normalize_str, strings)) + + def unpack_protected_keyword(name: str) -> str: """ By choice, this project aims to allow developers to access header or attribute in header by using the property From 51f3cf2c58634933e66d511543092d1827492d7e Mon Sep 17 00:00:00 2001 From: Ahmed TAHRI Date: Fri, 22 May 2020 00:37:18 +0200 Subject: [PATCH 5/8] :heavy_check_mark: Ensure that previous fixed regression never come back --- tests/test_attributes.py | 21 ++++++++++++--------- tests/test_header_operation.py | 16 ++++++++++++++++ tests/test_header_order.py | 10 ++++++++++ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/tests/test_attributes.py b/tests/test_attributes.py index da032c3..d728ef0 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -5,19 +5,22 @@ class AttributesTestCase(unittest.TestCase): def test_eq(self): - attr_a = Attributes(["a", "p=8a", "a", "XX"]) - attr_b = Attributes(["p=8a", "a", "a", "XX"]) - attr_c = Attributes(["p=8a", "a", "A", "Xx"]) - attr_d = Attributes(["p=8a", "a", "A", "Xx", "XX=a"]) - attr_e = Attributes(["p=8A", "a", "A", "Xx"]) + with self.subTest( + "Ensure that Attributes instances are compared the correct way" + ): + attr_a = Attributes(["a", "p=8a", "a", "XX"]) + attr_b = Attributes(["p=8a", "a", "a", "XX"]) + attr_c = Attributes(["p=8a", "a", "A", "Xx"]) + attr_d = Attributes(["p=8a", "a", "A", "Xx", "XX=a"]) + attr_e = Attributes(["p=8A", "a", "A", "Xx"]) - self.assertEqual(attr_a, attr_b) + self.assertEqual(attr_a, attr_b) - self.assertEqual(attr_a, attr_c) + self.assertEqual(attr_a, attr_c) - self.assertNotEqual(attr_a, attr_d) + self.assertNotEqual(attr_a, attr_d) - self.assertNotEqual(attr_a, attr_e) + self.assertNotEqual(attr_a, attr_e) if __name__ == "__main__": diff --git a/tests/test_header_operation.py b/tests/test_header_operation.py index 3738f09..38034cc 100644 --- a/tests/test_header_operation.py +++ b/tests/test_header_operation.py @@ -134,6 +134,22 @@ def test_contain_space_delimiter(self): self.assertFalse(authorization == "basic mysupersecrettoken") + def test_illegal_delitem_operation(self): + content_type = Header("Content-Type", 'text/html; charset="utf-8"') + + with self.subTest("Forbid to remove non-valued attr using delitem"): + with self.assertRaises(KeyError): + del content_type["text/html"] + + def test_attrs_access_case_insensitive(self): + + content_type = Header("Content-Type", 'text/html; charset="utf-8"') + + with self.subTest("Verify that attrs can be accessed no matter case"): + self.assertEqual("utf-8", content_type.charset) + self.assertEqual("utf-8", content_type.charseT) + self.assertEqual("utf-8", content_type.CHARSET) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_header_order.py b/tests/test_header_order.py index 40f5e23..4c9bfdc 100644 --- a/tests/test_header_order.py +++ b/tests/test_header_order.py @@ -39,6 +39,16 @@ def test_pop_negative_index(self): self.assertEqual(["a", "b", "h", "h"], header.attrs) + def test_attrs_original_case(self): + header = Header("Content-Type", "aA; bc=k; hA; h; zZzZ=0") + + with self.subTest( + "Ensure that attrs and valued_attrs properties keep the original case." + ): + self.assertEqual(["aA", "bc", "hA", "h", "zZzZ"], header.attrs) + + self.assertEqual(["bc", "zZzZ"], header.valued_attrs) + if __name__ == "__main__": unittest.main() From 1142f7095ca3cb0d7618f15c9b5fe8065b54b6d2 Mon Sep 17 00:00:00 2001 From: Ahmed TAHRI Date: Fri, 22 May 2020 00:41:18 +0200 Subject: [PATCH 6/8] :bookmark: Bump version to 2.2.1 --- kiss_headers/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiss_headers/version.py b/kiss_headers/version.py index 770628f..65ee7b6 100644 --- a/kiss_headers/version.py +++ b/kiss_headers/version.py @@ -2,5 +2,5 @@ Expose version """ -__version__ = "2.2.0" +__version__ = "2.2.1" VERSION = __version__.split(".") From 13fae079dde87c948882b8ff15018f8da9b52fb4 Mon Sep 17 00:00:00 2001 From: Ahmed TAHRI Date: Fri, 22 May 2020 00:45:05 +0200 Subject: [PATCH 7/8] :art: Apply isort on models.py --- kiss_headers/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiss_headers/models.py b/kiss_headers/models.py index e9ce7bf..2458b1b 100644 --- a/kiss_headers/models.py +++ b/kiss_headers/models.py @@ -8,12 +8,12 @@ header_content_split, header_name_to_class, is_legal_header_name, + normalize_list, normalize_str, prettify_header_name, unfold, unpack_protected_keyword, unquote, - normalize_list, ) OUTPUT_LOCK_TYPE: bool = False From bc1860b1d951ca388bbda85c5e75d5756c71eb8c Mon Sep 17 00:00:00 2001 From: Ahmed TAHRI Date: Fri, 22 May 2020 00:52:51 +0200 Subject: [PATCH 8/8] :bug: About same previous regression but on delitem --- kiss_headers/models.py | 3 ++- tests/test_header_operation.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/kiss_headers/models.py b/kiss_headers/models.py index 2458b1b..69a6e5f 100644 --- a/kiss_headers/models.py +++ b/kiss_headers/models.py @@ -322,7 +322,8 @@ def __delitem__(self, key: str) -> None: >>> str(headers.content_type) 'text/html' """ - if key not in normalize_list(self.valued_attrs): + + if normalize_str(key) not in normalize_list(self.valued_attrs): raise KeyError( "'{item}' attribute is not defined or have at least one value associated within '{header}' header.".format( item=key, header=self.name diff --git a/tests/test_header_operation.py b/tests/test_header_operation.py index 38034cc..7d5accf 100644 --- a/tests/test_header_operation.py +++ b/tests/test_header_operation.py @@ -150,6 +150,10 @@ def test_attrs_access_case_insensitive(self): self.assertEqual("utf-8", content_type.charseT) self.assertEqual("utf-8", content_type.CHARSET) + with self.subTest("Using del on attr using case insensitive key"): + del content_type.CHARSET + self.assertNotIn("charset", content_type) + if __name__ == "__main__": unittest.main()