Skip to content

Commit

Permalink
Merge pull request #34 from Ousret/develop
Browse files Browse the repository at this point in the history
🐛 Minor bugfix release 2.2.1
  • Loading branch information
Ousret authored May 21, 2020
2 parents 5c3d27c + bc1860b commit 7820c03
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 26 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<h1 align="center">Welcome to Headers for Humans 👋 <a href="https://twitter.com/intent/tweet?text=Python%20library%20for%20oriented%20object%20HTTP%20or%20IMAP%20headers.&url=https://www.github.com/Ousret/kiss-headers&hashtags=python,headers"><img src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"/></a></h1>
<h1 align="center">Welcome to Headers for Humans 👋 <a href="https://twitter.com/intent/tweet?text=Python%20library%20for%20oriented%20object%20HTTP%20style%20headers.&url=https://www.github.com/Ousret/kiss-headers&hashtags=python,headers,opensource"><img src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"/></a></h1>

<p align="center">
<sup>Object oriented headers, parser and builder.</sup><br>
Expand Down
37 changes: 22 additions & 15 deletions kiss_headers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
header_content_split,
header_name_to_class,
is_legal_header_name,
normalize_list,
normalize_str,
prettify_header_name,
unfold,
Expand Down Expand Up @@ -170,6 +171,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":
"""
Expand All @@ -196,7 +199,9 @@ 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
self._members.append(other)

return self

Expand Down Expand Up @@ -256,6 +261,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

Expand Down Expand Up @@ -304,6 +310,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:
"""
Expand All @@ -315,15 +322,17 @@ def __delitem__(self, key: str) -> None:
>>> str(headers.content_type)
'text/html'
"""
if key not in self._attrs:

if normalize_str(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
)
)

self._attrs.remove(key)
self._content = str(self._attrs)
self._members = header_content_split(self._content, ";")

def __delattr__(self, item: str) -> None:
"""
Expand All @@ -337,9 +346,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
)
)
Expand Down Expand Up @@ -395,14 +404,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']
"""
Expand All @@ -420,7 +427,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
Expand Down Expand Up @@ -458,7 +465,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
Expand Down Expand Up @@ -488,11 +495,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
)
)
Expand All @@ -513,9 +520,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
)
)
Expand Down
5 changes: 5 additions & 0 deletions kiss_headers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion kiss_headers/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
Expose version
"""

__version__ = "2.2.0"
__version__ = "2.2.1"
VERSION = __version__.split(".")
21 changes: 12 additions & 9 deletions tests/test_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down
20 changes: 20 additions & 0 deletions tests/test_header_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@ 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)

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()
10 changes: 10 additions & 0 deletions tests/test_header_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

0 comments on commit 7820c03

Please sign in to comment.