diff --git a/python-mixins/README.md b/python-mixins/README.md new file mode 100644 index 0000000000..bc6f62567c --- /dev/null +++ b/python-mixins/README.md @@ -0,0 +1,3 @@ +# What Are Mixin Classes in Python? + +This folder contains sample code for the Real Python tutorial [What Are Mixin Classes in Python?](https://realpython.com/python-mixin/). \ No newline at end of file diff --git a/python-mixins/mixins.py b/python-mixins/mixins.py new file mode 100644 index 0000000000..54230ba4dd --- /dev/null +++ b/python-mixins/mixins.py @@ -0,0 +1,90 @@ +import json +from typing import Self + + +class SerializableMixin: + def serialize(self) -> dict: + if hasattr(self, "__slots__"): + return {name: getattr(self, name) for name in self.__slots__} + else: + return vars(self) + + +class JSONSerializableMixin: + @classmethod + def from_json(cls, json_string: str) -> Self: + return cls(**json.loads(json_string)) + + def as_json(self) -> str: + return json.dumps(vars(self)) + + +class TypedKeyMixin: + def __init__(self, key_type, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__type = key_type + + def __setitem__(self, key, value): + if not isinstance(key, self.__type): + raise TypeError(f"key must be {self.__type} but was {type(key)}") + super().__setitem__(key, value) + + +class TypedValueMixin: + def __init__(self, value_type, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__type = value_type + + def __setitem__(self, key, value): + if not isinstance(value, self.__type): + if not isinstance(value, self.__type): + raise TypeError( + f"value must be {self.__type} but was {type(value)}" + ) + super().__setitem__(key, value) + + +if __name__ == "__main__": + from collections import UserDict + from dataclasses import dataclass + from pathlib import Path + from types import SimpleNamespace + + @dataclass + class User(JSONSerializableMixin): + user_id: int + email: str + + class AppSettings(JSONSerializableMixin, SimpleNamespace): + def save(self, filepath: str | Path) -> None: + Path(filepath).write_text(self.as_json(), encoding="utf-8") + + class Inventory(TypedKeyMixin, TypedValueMixin, UserDict): + pass + + user = User(555, "jdoe@example.com") + print(user.as_json()) + + settings = AppSettings() + settings.host = "localhost" + settings.port = 8080 + settings.debug_mode = True + settings.log_file = None + settings.urls = ( + "https://192.168.1.200:8000", + "https://192.168.1.201:8000", + ) + settings.save("settings.json") + + fruits = Inventory(str, int) + fruits["apples"] = 42 + + try: + fruits["🍌".encode("utf-8")] = 15 + except TypeError as ex: + print(ex) + + try: + fruits["bananas"] = 3.5 + except TypeError as ex: + print(ex) diff --git a/python-mixins/stateful_v1.py b/python-mixins/stateful_v1.py new file mode 100644 index 0000000000..46ea7c2d8e --- /dev/null +++ b/python-mixins/stateful_v1.py @@ -0,0 +1,41 @@ +class TypedKeyMixin: + key_type = object + + def __setitem__(self, key, value): + if not isinstance(key, self.key_type): + raise TypeError(f"key must be {self.key_type} but was {type(key)}") + super().__setitem__(key, value) + + +class TypedValueMixin: + value_type = object + + def __setitem__(self, key, value): + if not isinstance(value, self.value_type): + raise TypeError( + f"value must be {self.value_type} but was {type(value)}" + ) + super().__setitem__(key, value) + + +if __name__ == "__main__": + from collections import UserDict + + class Inventory(TypedKeyMixin, TypedValueMixin, UserDict): + key_type = str + value_type = int + + fruits = Inventory() + fruits["apples"] = 42 + + try: + fruits["🍌".encode("utf-8")] = 15 + except TypeError as ex: + print(ex) + + try: + fruits["bananas"] = 3.5 + except TypeError as ex: + print(ex) + + print(f"{vars(fruits) = }") diff --git a/python-mixins/stateful_v2.py b/python-mixins/stateful_v2.py new file mode 100644 index 0000000000..f758eff851 --- /dev/null +++ b/python-mixins/stateful_v2.py @@ -0,0 +1,43 @@ +def TypedKeyMixin(key_type=object): + class _: + def __setitem__(self, key, value): + if not isinstance(key, key_type): + raise TypeError(f"key must be {key_type} but was {type(key)}") + super().__setitem__(key, value) + + return _ + + +def TypedValueMixin(value_type=object): + class _: + def __setitem__(self, key, value): + if not isinstance(value, value_type): + raise TypeError( + f"value must be {value_type} but was {type(value)}" + ) + super().__setitem__(key, value) + + return _ + + +if __name__ == "__main__": + from collections import UserDict + + class Inventory(TypedKeyMixin(str), TypedValueMixin(int), UserDict): + key_type = "This attribute has nothing to collide with" + + fruits = Inventory() + fruits["apples"] = 42 + + try: + fruits["🍌".encode("utf-8")] = 15 + except TypeError as ex: + print(ex) + + try: + fruits["bananas"] = 3.5 + except TypeError as ex: + print(ex) + + print(f"{vars(fruits) = }") + print(f"{Inventory.key_type = }") diff --git a/python-mixins/stateful_v3.py b/python-mixins/stateful_v3.py new file mode 100644 index 0000000000..45bbc67d00 --- /dev/null +++ b/python-mixins/stateful_v3.py @@ -0,0 +1,57 @@ +def key_type(expected_type): + def class_decorator(cls): + setitem = cls.__setitem__ + + def __setitem__(self, key, value): + if not isinstance(key, expected_type): + raise TypeError( + f"key must be {expected_type} but was {type(key)}" + ) + return setitem(self, key, value) + + cls.__setitem__ = __setitem__ + return cls + + return class_decorator + + +def value_type(expected_type): + def class_decorator(cls): + setitem = cls.__setitem__ + + def __setitem__(self, key, value): + if not isinstance(value, expected_type): + raise TypeError( + f"value must be {expected_type} but was {type(value)}" + ) + return setitem(self, key, value) + + cls.__setitem__ = __setitem__ + return cls + + return class_decorator + + +if __name__ == "__main__": + from collections import UserDict + + @key_type(str) + @value_type(int) + class Inventory(UserDict): + key_type = "This attribute has nothing to collide with" + + fruits = Inventory() + fruits["apples"] = 42 + + try: + fruits["🍌".encode("utf-8")] = 15 + except TypeError as ex: + print(ex) + + try: + fruits["bananas"] = 3.5 + except TypeError as ex: + print(ex) + + print(f"{vars(fruits) = }") + print(f"{Inventory.key_type = }") diff --git a/python-mixins/typed_dict.py b/python-mixins/typed_dict.py new file mode 100644 index 0000000000..04304a70e7 --- /dev/null +++ b/python-mixins/typed_dict.py @@ -0,0 +1,59 @@ +def typed_dict(key_type=object, value_type=object): + def class_decorator(cls): + setitem = cls.__setitem__ + + def __setitem__(self, key, value): + if not isinstance(key, key_type): + raise TypeError( + f"value must be {key_type} but was {type(key)}" + ) + + if not isinstance(value, value_type): + raise TypeError( + f"value must be {value_type} but was {type(value)}" + ) + + setitem(self, key, value) + + cls.__setitem__ = __setitem__ + + return cls + + return class_decorator + + +if __name__ == "__main__": + from collections import UserDict + + # Enforce str keys and int values: + @typed_dict(str, int) + class Inventory(UserDict): + pass + + # Enforce str keys, allow any value type: + @typed_dict(str) + class AppSettings(UserDict): + pass + + fruits = Inventory() + fruits["apples"] = 42 + + try: + fruits["🍌".encode("utf-8")] = 15 + except TypeError as ex: + print(ex) + + try: + fruits["bananas"] = 3.5 + except TypeError as ex: + print(ex) + + settings = AppSettings() + settings["host"] = "localhost" + settings["port"] = 8080 + settings["debug_mode"] = True + + try: + settings[b"binary data"] = "nope" + except TypeError as ex: + print(ex) diff --git a/python-mixins/utils.py b/python-mixins/utils.py new file mode 100644 index 0000000000..57058ff52b --- /dev/null +++ b/python-mixins/utils.py @@ -0,0 +1,44 @@ +from collections import UserDict + + +class DebugMixin: + def __setitem__(self, key, value): + super().__setitem__(key, value) + print(f"Item set: {key=!r}, {value=!r}") + + def __delitem__(self, key): + super().__delitem__(key) + print(f"Item deleted: {key=!r}") + + +class CaseInsensitiveDict(DebugMixin, UserDict): + def __setitem__(self, key: str, value: str) -> None: + super().__setitem__(key.lower(), value) + + def __getitem__(self, key: str) -> str: + return super().__getitem__(key.lower()) + + def __delitem__(self, key: str) -> None: + super().__delitem__(key.lower()) + + def __contains__(self, key: str) -> bool: + return super().__contains__(key.lower()) + + def get(self, key: str, default: str = "") -> str: + return super().get(key.lower(), default) + + +if __name__ == "__main__": + from pprint import pp + + headers = CaseInsensitiveDict() + headers["Content-Type"] = "application/json" + headers["Cookie"] = "csrftoken=a4f3c7d28c194e5b; sessionid=f92e4b7c6" + + print(f"{headers["cookie"] = }") + print(f"{"CooKIE" in headers = }") + + del headers["Cookie"] + print(f"{headers = }") + + pp(CaseInsensitiveDict.__mro__)