3.3. OOP Classmethod

  • Using class as namespace

  • Will pass class as a first argument

  • self is not required

In Python, classmethod is a built-in decorator that defines a method that belongs to the class rather than to an instance of the class. This means that the method can be called on the class itself, rather than on an instance of the class.

A classmethod takes a cls parameter as its first argument, which refers to the class itself, rather than to an instance of the class. This allows the method to access and modify class-level attributes and methods.

Here's an example of using the classmethod decorator to define a method that returns the number of instances of a class:

>>> class MyClass:
...     count = 0
...
...     def __init__(self):
...         MyClass.count += 1
...
...     @classmethod
...     def get_count(cls):
...         return cls.count
>>>
>>> # Create some instances of MyClass
>>> obj1 = MyClass()
>>> obj2 = MyClass()
>>> obj3 = MyClass()
>>>
>>> # Call the classmethod on the class itself
>>> print(MyClass.get_count())
3

In this example, the MyClass class defines a class-level attribute count and a classmethod called get_count(). The __init__() method of the class increments the count attribute each time a new instance of the class is created.

The get_count() method is decorated with the classmethod decorator, which means that it can be called on the class itself, rather than on an instance of the class. The method returns the value of the count attribute, which represents the number of instances of the class that have been created.

The get_count() method takes a cls parameter as its first argument, which refers to the class itself. This allows the method to access the count attribute of the class and return its value.

Dynamic methods:

>>> class User:
...     def say_hello(self):
...         pass

Static methods:

>>> class User:
...     def say_hello():
...         pass

Static and dynamic method:

>>> class User:
...     @staticmethod
...     def say_hello():
...         pass

Class methods:

>>> class User:
...     @classmethod
...     def say_hello(cls):
...         pass

3.3.1. Manifestation

>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...
...     @staticmethod
...     def from_json(data):
...         data = json.loads(data)
...         return User(**data)
>>>
>>> data = '{"firstname": "Mark", "lastname": "Watney"}'
>>> result = User.from_json(data)
>>>
>>> print(result)
User(firstname='Mark', lastname='Watney')
>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> class JSONMixin:
...     @staticmethod
...     def from_json(data):
...         data = json.loads(data)
...         return User(**data)
>>>
>>>
>>> @dataclass
... class User(JSONMixin):
...     firstname: str
...     lastname: str
>>>
>>>
>>> data = '{"firstname": "Mark", "lastname": "Watney"}'
>>> result = User.from_json(data)
>>>
>>> print(result)
User(firstname='Mark', lastname='Watney')
>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> class JSONMixin:
...     @classmethod
...     def from_json(cls, data):
...         data = json.loads(data)
...         return cls(**data)
>>>
>>> @dataclass
... class User(JSONMixin):
...     firstname: str
...     lastname: str
>>>
>>>
>>> data = '{"firstname": "Mark", "lastname": "Watney"}'
>>> result = User.from_json(data)
>>>
>>> print(result)
User(firstname='Mark', lastname='Watney')

3.3.2. Use Case - 0x01

>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> class JSONMixin:
...     @classmethod
...     def from_json(cls, data):
...         data = json.loads(data)
...         return cls(**data)
>>>
>>> @dataclass
... class User(JSONMixin):
...     firstname: str
...     lastname: str
>>>
>>> @dataclass
... class Admin(JSONMixin):
...     firstname: str
...     lastname: str
>>>
>>>
>>> data = '{"firstname": "Mark", "lastname": "Watney"}'
>>>
>>> User.from_json(data)
User(firstname='Mark', lastname='Watney')
>>>
>>> Admin.from_json(data)
Admin(firstname='Mark', lastname='Watney')

3.3.3. Use Case - 0x02

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Account:
...     firstname: str = None
...     lastname: str = None
...
...     @classmethod
...     def from_json(cls, data):
...         import json
...         data = json.loads(data)
...         return cls(**data)
>>>
>>> class User(Account):
...     pass
>>>
>>> class Admin(Account):
...     pass
>>>
>>>
>>> data = '{"firstname": "Mark", "lastname": "Watney"}'
>>>
>>> User.from_json(data)
User(firstname='Mark', lastname='Watney')
>>>
>>> Admin.from_json(data)
Admin(firstname='Mark', lastname='Watney')

3.3.4. Use Case - 0x03

  • Singleton

>>> class Singleton:
...     _instance: object
...
...     @classmethod
...     def get_instance(cls):
...         if not hasattr(cls, '_instance'):
...             cls._instance = object.__new__(cls)
...         return cls._instance
>>>
>>>
>>> class DatabaseConnection(Singleton):
...     pass
>>>
>>>
>>> db1 = DatabaseConnection.get_instance()
>>> db2 = DatabaseConnection.get_instance()
>>>
>>>
>>> print(db1)  
<__main__.DatabaseConnection object at 0x102453ee0>
>>>
>>> print(db2)  
<__main__.DatabaseConnection object at 0x102453ee0>

3.3.5. Use Case - 0x04

File myapp/timezone.py:

>>> class AbstractTimezone:
...     tzname: str
...     tzcode: str
...
...     def __init__(self, date, time):
...         ...
...
...     @classmethod
...     def from_string(cls, string):
...         values = datetime.fromisoformat(string)
...         return cls(**values)
>>>
>>>
>>> class CET(AbstractTimezone):
...     tzname = 'Central European Time'
...     tzcode = 'CET'
>>>
>>> class CEST(AbstractTimezone):
...     tzname = 'Central European Summer Time'
...     tzcode = 'CEST'

Operating system:

export TIMEZONE=CET

File: myapp/settings.py:

>>> 
... import myapp.timezone
... from os import getenv
...
... time = getattr(myapp.timezone, getenv('TIMEZONE'))

File myapp/usage.py:

>>> 
... from myapp.settings import time
...
... dt = time.from_string('1969-07-21T02:53:15Z')
... print(dt.tzname)
Central European Time

3.3.6. Use Case - 0x05

>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...
...     @classmethod
...     def from_json(cls, string: str):
...         data = json.loads(string)
...         return cls(**data)
...
...     @classmethod
...     def from_csv(cls, string: str):
...         data = string.split(',')
...         return cls(*data)
...
...     @classmethod
...     def from_dict(cls, data: dict):
...         return cls(**data)
...
...     @classmethod
...     def from_list(cls, data: list):
...         return cls(*data)
...
...     @classmethod
...     def from_tuple(cls, data: tuple):
...         return cls(*data)
>>>
>>>
>>> @dataclass
... class Admin(User):
...     pass
>>> DATA = '{"firstname":"Mark","lastname":"Watney"}'
>>>
>>> mark = User.from_json(DATA)
>>> mark
User(firstname='Mark', lastname='Watney)
>>>
>>> melissa = Admin.from_json(DATA)
>>> melissa
Admin(firstname='Mark', lastname='Watney)

3.3.7. Assignments

Code 3.59. Solution
"""
* Assignment: OOP MethodClassmethod FromTuple
* Complexity: easy
* Lines of code: 3 lines
* Time: 3 min

English:
    1. Define class `Book` with:
        a. Field `title: str`
        b. Field `author: str`
        c. Method `from_tuple()`
    2. Method `from_tuple()`:
        a. Parameter `data: tuple[str,str]`, example: ('Martian', 'Andy Weir')
        b. Returns instance of a class on which was called
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Book` z:
        a. Polem `title: str`
        b. Polem `author: str`
        c. Metodą `from_tuple()`
    2. Metoda `from_tuple()`:
        a. Parametr `data: tuple[str,str]`, przykład: ('Martian', 'Andy Weir')
        b. Zwraca instancję klasy na której została wykonana
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(Book)
    >>> assert isclass(ScienceFiction)
    >>> assert isclass(History)
    >>> assert isclass(Adventure)

    >>> MARTIAN = ('Martian', 'Andy Weir')
    >>> martian = ScienceFiction.from_tuple(MARTIAN)
    >>> assert type(martian.title) is str
    >>> assert type(martian.author) is str
    >>> assert martian.title == 'Martian'
    >>> assert martian.author == 'Andy Weir'

    >>> DUNE = ('Dune', 'Frank Herbert')
    >>> dune = Adventure.from_tuple(DUNE)
    >>> assert type(dune.title) is str
    >>> assert type(dune.author) is str
    >>> assert dune.title == 'Dune'
    >>> assert dune.author == 'Frank Herbert'

    >>> RIGHT_STUFF = ('The Right Stuff', 'Tom Wolfe')
    >>> right_stuff = History.from_tuple(RIGHT_STUFF)
    >>> assert type(right_stuff.title) is str
    >>> assert type(right_stuff.author) is str
    >>> assert right_stuff.title == 'The Right Stuff'
    >>> assert right_stuff.author == 'Tom Wolfe'
"""


class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    # parameter: `data: tuple[str,str]`
    # example: ('Martian', 'Andy Weir')
    # return: instance of a class on which was called
    # type: Callable[[type[Self], tuple[str, str]], Self]
    def from_tuple():
        ...


class ScienceFiction(Book):
    pass


class History(Book):
    pass


class Adventure(Book):
    pass


Code 3.60. Solution
"""
* Assignment: OOP MethodClassmethod FromCsv
* Complexity: easy
* Lines of code: 4 lines
* Time: 3 min

English:
    1. Define class `Iris` with:
        a. Field `sepal_length: float`
        b. Field `sepal_width: float`
        c. Field `petal_length: float`
        d. Field `petal_width: float`
        e. Field `species: str`
        f. Method `from_csv()`
    2. Method `from_csv()`:
        a. Parameter `data: str`, example: '5.8,2.7,5.1,1.9,virginica'
        b. Returns instance of a class on which was called
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Iris` z:
        a. Polem `sepal_length: float`
        b. Polem `sepal_width: float`
        c. Polem `petal_length: float`
        d. Polem `petal_width: float`
        e. Polem `species: str`
        f. Metodą `from_csv()`
    2. Metoda `from_csv()`:
        a. Parametr `data: str`, przykład: '5.8,2.7,5.1,1.9,virginica'
        b. Zwraca instancję klasy na której została wykonana
    3. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * str.split()

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(Iris)
    >>> assert isclass(Setosa)
    >>> assert isclass(Versicolor)
    >>> assert isclass(Virginica)

    >>> data = '5.8,2.7,5.1,1.9,virginica'
    >>> virginica = Virginica.from_csv(data)
    >>> assert type(virginica.sepal_length) is float
    >>> assert type(virginica.sepal_width) is float
    >>> assert type(virginica.petal_length) is float
    >>> assert type(virginica.petal_width) is float
    >>> assert type(virginica.species) is str
    >>> assert virginica.sepal_length == 5.8
    >>> assert virginica.sepal_width == 2.7
    >>> assert virginica.petal_length == 5.1
    >>> assert virginica.petal_width == 1.9
    >>> assert virginica.species == 'virginica'

    >>> data = '5.1,3.5,1.4,0.2,setosa'
    >>> setosa = Setosa.from_csv(data)
    >>> assert type(setosa.sepal_length) is float
    >>> assert type(setosa.sepal_width) is float
    >>> assert type(setosa.petal_length) is float
    >>> assert type(setosa.petal_width) is float
    >>> assert type(setosa.species) is str
    >>> assert setosa.sepal_length == 5.1
    >>> assert setosa.sepal_width == 3.5
    >>> assert setosa.petal_length == 1.4
    >>> assert setosa.petal_width == 0.2
    >>> assert setosa.species == 'setosa'

    >>> data = '5.7,2.8,4.1,1.3,versicolor'
    >>> versicolor = Versicolor.from_csv(data)
    >>> assert type(versicolor.sepal_length) is float
    >>> assert type(versicolor.sepal_width) is float
    >>> assert type(versicolor.petal_length) is float
    >>> assert type(versicolor.petal_width) is float
    >>> assert type(versicolor.species) is str
    >>> assert versicolor.sepal_length == 5.7
    >>> assert versicolor.sepal_width == 2.8
    >>> assert versicolor.petal_length == 4.1
    >>> assert versicolor.petal_width == 1.3
    >>> assert versicolor.species == 'versicolor'
"""

class Iris:
    def __init__(self, sepal_length, sepal_width,
                 petal_length, petal_width, species):
        self.sepal_length = sepal_length
        self.sepal_width = sepal_width
        self.petal_length = petal_length
        self.petal_width = petal_width
        self.species = species

    # parameter: `data: str`
    # example: '5.8,2.7,5.1,1.9,virginica'
    # return: instance of a class on which was called
    # type: Callable[[type[Self], str], Self]
    def from_csv():
        ...


class Setosa(Iris):
    pass


class Virginica(Iris):
    pass


class Versicolor(Iris):
    pass


Code 3.61. Solution
"""
* Assignment: OOP MethodClassmethod FromString
* Complexity: easy
* Lines of code: 4 lines
* Time: 5 min

English:
    1. Define class `Account` with:
        a. Field `username: str`
        b. Field `password: str`
        c. Field `uid: int`
        d. Field `gid: int`
        e. Field `gecos: str`
        f. Field `home: str`
        g. Field `shell: str`
        h. Method `from_passwd()`
    2. Method `from_passwd()`:
        a. Parameter `data: str`, example: 'root:x:0:0:root:/root:/bin/bash'
        b. Returns instance of a class on which was called
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Account` z:
        a. Polem `username: str`
        b. Polem `password: str`
        c. Polem `uid: int`
        d. Polem `gid: int`
        e. Polem `gecos: str`
        f. Polem `home: str`
        g. Polem `shell: str`
        h. Metodą `from_passwd()`
    2. Metoda `from_passwd()`:
        a. Parametr `data: str`, przykład: 'root:x:0:0:root:/root:/bin/bash'
        b. Zwraca instancję klasy na której została wykonana
    3. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * str.split()
    * int()

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(Account)
    >>> assert isclass(SystemAccount)
    >>> assert isclass(UserAccount)

    >>> data = 'root:x:0:0:root:/root:/bin/bash'
    >>> admin = SystemAccount.from_passwd(data)
    >>> assert type(admin.username) is str
    >>> assert type(admin.password) is str
    >>> assert type(admin.uid) is int
    >>> assert type(admin.gid) is int
    >>> assert type(admin.gecos) is str
    >>> assert type(admin.home) is str
    >>> assert type(admin.shell) is str
    >>> assert admin.username == 'root'
    >>> assert admin.password == 'x'
    >>> assert admin.uid == 0
    >>> assert admin.gid == 0
    >>> assert admin.gecos == 'root'
    >>> assert admin.home == '/root'
    >>> assert admin.shell == '/bin/bash'

    >>> data = 'watney:x:1000:1000:Mark Watney:/home/watney:/bin/bash'
    >>> user = UserAccount.from_passwd(data)
    >>> assert type(user.username) is str
    >>> assert type(user.password) is str
    >>> assert type(user.uid) is int
    >>> assert type(user.gid) is int
    >>> assert type(user.gecos) is str
    >>> assert type(user.home) is str
    >>> assert type(user.shell) is str
    >>> assert user.username == 'watney'
    >>> assert user.password == 'x'
    >>> assert user.uid == 1000
    >>> assert user.gid == 1000
    >>> assert user.gecos == 'Mark Watney'
    >>> assert user.home == '/home/watney'
    >>> assert user.shell == '/bin/bash'
"""

class Account:
    def __init__(self, username, password, uid, gid, gecos, home, shell):
        self.username = username
        self.password = password
        self.uid = uid
        self.gid = gid
        self.gecos = gecos
        self.home = home
        self.shell = shell

    # parameter: `data: str`
    # example: 'root:x:0:0:root:/root:/bin/bash'
    # hint: uid and gid must be int
    # return: instance of a class on which was called
    # type: Callable[[type[Self], str], Self]
    def from_passwd():
        ...


class SystemAccount(Account):
    pass


class UserAccount(Account):
    pass


Code 3.62. Solution
"""
* Assignment: OOP MethodClassmethod FromDict
* Complexity: easy
* Lines of code: 4 lines
* Time: 3 min

English:
    1. Define class `Animal` with:
        a. Field `english_name: str`
        b. Field `latin_name: str`
        c. Method `from_dict()`
    2. Method `from_dict()`:
        a. Parameter `data: dict[str,str]`, example: {'english_name': 'Cat', 'latin_name': 'Felis catus'}
        b. Returns instance of a class on which was called
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Animal` z:
        a. Polem `english_name: str`
        b. Polem `latin_name: str`
        c. Metodą `from_dict()`
    2. Metoda `from_dict()`:
        a. Parametr `data: dict[str,str]`, przykład: {'english_name': 'Cat', 'latin_name': 'Felis catus'}
        b. Zwraca instancję klasy na której została wykonana
    3. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * dict.get()

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass
    >>> from types import NoneType

    >>> assert isclass(Animal)
    >>> assert isclass(Cat)
    >>> assert isclass(Dog)

    >>> CAT = {'english_name': 'Cat', 'latin_name': 'Felis catus'}
    >>> cat = Cat.from_dict(CAT)
    >>> assert type(cat.english_name) is str
    >>> assert type(cat.latin_name) is str
    >>> assert cat.english_name == 'Cat'
    >>> assert cat.latin_name == 'Felis catus'

    >>> DOG = {'english_name': 'Dog', 'latin_name': 'Canis familiaris'}
    >>> dog = Dog.from_dict(DOG)
    >>> assert type(dog.english_name) is str
    >>> assert type(dog.latin_name) is str
    >>> assert dog.english_name == 'Dog'
    >>> assert dog.latin_name == 'Canis familiaris'

    >>> PLATYPUS = {'english_name': 'Platypus'}
    >>> platypus = Platypus.from_dict(PLATYPUS)
    >>> assert type(platypus.english_name) is str
    >>> assert type(platypus.latin_name) is NoneType
    >>> assert platypus.english_name == 'Platypus'
    >>> assert platypus.latin_name is None
"""


class Animal:
    def __init__(self, english_name, latin_name):
        self.english_name = english_name
        self.latin_name = latin_name

    # parameter: `data: dict[str,str]`
    # example: {'english_name': 'Cat', 'latin_name': 'Felis catus'}
    # hint: Platypus has `latin_name` field empty (None)
    # return: instance of a class on which was called
    # type: Callable[[type[Self], dict[str,str]], Self]
    def from_dict():
        ...


class Cat(Animal):
    pass


class Dog(Animal):
    pass


class Platypus(Animal):
    pass


Code 3.63. Solution
"""
* Assignment: OOP MethodClassmethod FromJson
* Complexity: easy
* Lines of code: 4 lines
* Time: 3 min

English:
    1. Define class `Movie` with:
        a. Field `title: str`
        b. Field `director: str`
        c. Method `from_json()`
    2. Method `from_json()`:
        a. Parameter `data: str`, example: '{"title":"Martian","director":"Ridley Scott"}'
        b. Returns instance of a class on which was called
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Movie` z:
        a. Polem `title: str`
        b. Polem `director: str`
        c. Metodą `from_json()`
    2. Metoda `from_json()`:
        a. Parametr `data: str`, przykład: '{"title":"Martian","director":"Ridley Scott"}'
        b. Zwraca instancję klasy na której została wykonana
    3. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * json.loads()

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(Movie)
    >>> assert isclass(ScienceFiction)
    >>> assert isclass(History)
    >>> assert isclass(Adventure)

    >>> MARTIAN = '{"title":"Martian","director":"Ridley Scott"}'
    >>> martian = ScienceFiction.from_json(MARTIAN)
    >>> assert type(martian.title) is str
    >>> assert type(martian.director) is str
    >>> assert martian.title == 'Martian'
    >>> assert martian.director == 'Ridley Scott'

    >>> DUNE = '{"title":"Dune","director":"Denis Villeneuve"}'
    >>> dune = Adventure.from_json(DUNE)
    >>> assert type(dune.title) is str
    >>> assert type(dune.director) is str
    >>> assert dune.title == 'Dune'
    >>> assert dune.director == 'Denis Villeneuve'

    >>> RIGHT_STUFF = '{"title":"The Right Stuff","director":"Philip Kaufman"}'
    >>> right_stuff = History.from_json(RIGHT_STUFF)
    >>> assert type(right_stuff.title) is str
    >>> assert type(right_stuff.director) is str
    >>> assert right_stuff.title == 'The Right Stuff'
    >>> assert right_stuff.director == 'Philip Kaufman'
"""
import json


class Movie:
    def __init__(self, title, director):
        self.title = title
        self.director = director

    # parameter: `data: str`
    # example: '{"title":"Martian","director":"Ridley Scott"}'
    # return: instance of a class on which was called
    # type: Callable[[type[Self], str], Self]
    def from_json():
        ...


class ScienceFiction(Movie):
    pass


class History(Movie):
    pass


class Adventure(Movie):
    pass


Code 3.64. Solution
"""
* Assignment: OOP MethodClassmethod FromDatetime
* Complexity: easy
* Lines of code: 10 lines
* Time: 5 min

English:
    1. Define class `DateTime` with:
        a. Field `year: int`
        b. Field `month: int`
        c. Field `day: int`
        d. Field `hour: int`
        e. Field `minute: int`
        f. Field `second: int`
        g. Field `tzinfo: str`
        h. Method `from_datetime()`
    2. Method `from_datetime()`:
        a. Parameter `dt: datetime`, example: datetime(1969, 7, 21, 2, 56, 15)
        b. Returns instance of a class on which was called
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `DateTime` z:
        a. Polem `year: int`
        b. Polem `month: int`
        c. Polem `day: int`
        d. Polem `hour: int`
        e. Polem `minute: int`
        f. Polem `second: int`
        g. Polem `tzinfo: str`
        h. Metodą `from_datetime()`
    2. Metoda `from_datetime()`:
        a. Parametr `dt: datetime`, przykład: datetime(1969, 7, 21, 2, 56, 15)
        b. Zwraca instancję klasy na której została wykonana
    3. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * zoneinfo.ZoneInfo()
    * datetime.datetime()

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(DateTime)
    >>> assert isclass(UTC)
    >>> assert isclass(CET)
    >>> assert isclass(CEST)

    >>> data = datetime(1969, 7, 21, 2, 56, 15)
    >>> utc = UTC.from_datetime(data)
    >>> assert type(utc.year) is int
    >>> assert type(utc.month) is int
    >>> assert type(utc.day) is int
    >>> assert type(utc.hour) is int
    >>> assert type(utc.minute) is int
    >>> assert type(utc.second) is int
    >>> assert type(utc.tzinfo) is ZoneInfo
    >>> assert utc.year == 1969
    >>> assert utc.month == 7
    >>> assert utc.day == 21
    >>> assert utc.hour == 2
    >>> assert utc.minute == 56
    >>> assert utc.second == 15
    >>> assert utc.tzinfo == ZoneInfo('Etc/UTC')

    >>> data = datetime(1969, 7, 21, 2, 56, 15)
    >>> cet = CET.from_datetime(data)
    >>> assert type(cet.year) is int
    >>> assert type(cet.month) is int
    >>> assert type(cet.day) is int
    >>> assert type(cet.hour) is int
    >>> assert type(cet.minute) is int
    >>> assert type(cet.second) is int
    >>> assert type(cet.tzinfo) is ZoneInfo
    >>> assert cet.year == 1969
    >>> assert cet.month == 7
    >>> assert cet.day == 21
    >>> assert cet.hour == 2
    >>> assert cet.minute == 56
    >>> assert cet.second == 15
    >>> assert cet.tzinfo == ZoneInfo('Etc/GMT-1')

    >>> data = datetime(1969, 7, 21, 2, 56, 15)
    >>> cest = CEST.from_datetime(data)
    >>> assert type(cest.year) is int
    >>> assert type(cest.month) is int
    >>> assert type(cest.day) is int
    >>> assert type(cest.hour) is int
    >>> assert type(cest.minute) is int
    >>> assert type(cest.second) is int
    >>> assert type(cest.tzinfo) is ZoneInfo
    >>> assert cest.year == 1969
    >>> assert cest.month == 7
    >>> assert cest.day == 21
    >>> assert cest.hour == 2
    >>> assert cest.minute == 56
    >>> assert cest.second == 15
    >>> assert cest.tzinfo == ZoneInfo('Etc/GMT-2')
"""

from datetime import datetime
from zoneinfo import ZoneInfo


class DateTime:
    tzname: str

    def __init__(self, year, month, day, hour, minute, second, tzinfo):
        self.year = year
        self.month = month
        self.day = day
        self.hour = hour
        self.minute = minute
        self.second = second
        self.tzinfo = tzinfo

    # parameter: `dt: datetime`
    # example: datetime(1969, 7, 21, 2, 56, 15)
    # hint: tzinfo = ZoneInfo(cls.tzname)
    # return: instance of a class on which was called
    # type: Callable[[type[Self], datetime], Self]
    def from_datetime():
        ...

class UTC(DateTime):
    tzname = 'Etc/UTC'


class CET(DateTime):
    tzname = 'Etc/GMT-1'


class CEST(DateTime):
    tzname = 'Etc/GMT-2'


Code 3.65. Solution
"""
* Assignment: OOP MethodClassmethod FromTimestamp
* Complexity: easy
* Lines of code: 4 lines
* Time: 3 min

English:
    1. Define class `Timezone` with:
        a. Field `timesamp: int`
        b. Field `tzname: str`
        c. Method `from_timestamp()`
    2. Method `from_timestamp()`:
        a. Parameter `data: int`, example: 1234567890
        b. Returns instance of a class on which was called
    3. Use `timezone.utc` as a parameter to `datetime.fromtimestamp()`
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Timezone` z:
        a. Polem `timesamp: int`
        b. Polem `tzname: str`
        c. Metodą `from_timestamp()`
    2. Metoda `from_timestamp()`:
        a. Parametr `data: int`, przykład: 1234567890
        b. Zwraca instancję klasy na której została wykonana
    3. Użyj `timezone.utc` jako parametr do `datetime.fromtimestamp()`
    4. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * datetime.fromtimestamp(tz=timezone.utc)

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(Timezone)
    >>> assert isclass(CET)
    >>> assert isclass(CEST)

    >>> cet = CET.from_timestamp(1234567890)
    >>> assert type(cet.tzname) is str
    >>> assert type(cet.dt) is datetime
    >>> assert cet.tzname == 'Central European Time'
    >>> assert cet.dt == datetime(2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc)

    >>> cest = CEST.from_timestamp(1234567890)
    >>> assert type(cest.tzname) is str
    >>> assert type(cest.dt) is datetime
    >>> assert cest.tzname == 'Central European Summer Time'
    >>> assert cest.dt == datetime(2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc)
"""
from datetime import datetime, timezone
from zoneinfo import ZoneInfo


class Timezone:
    tzname: str
    dt: datetime

    def __init__(self, dt):
        self.dt = dt

    # hint: use datetime.fromtimestamp with tz=timezone.utc
    # parameter: `data: int`
    # return: instance of a class on which was called
    # type: Callable[[type[Timezone], int], Timezone]
    def from_timestamp():
        ...


class CET(Timezone):
    tzname = 'Central European Time'


class CEST(Timezone):
    tzname = 'Central European Summer Time'


Code 3.66. Solution
"""
* Assignment: OOP MethodClassmethod FromToml
* Complexity: easy
* Lines of code: 6 lines
* Time: 8 min

English:
    1. Define class `Character` with:
        a. Field `character_class: str`
        b. Field `race: str`
        c. Field `alignment: str`
        d. Field `strength: int`
        e. Field `dexterity: int`
        f. Field `constitution: int`
        g. Field `intelligence: int`
        h. Field `wisdom: int`
        i. Field `charisma: int`
        j. Method `from_toml()`
    2. Method `from_toml()`:
        a. Parameter `filename: str`, example: 'myfile.toml'
        b. Parameter `name: str`, example: 'Sarevok'
        c. Returns instance of a class on which was called
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Character` z:
        a. Polem `character_class: str`
        b. Polem `race: str`
        c. Polem `alignment: str`
        d. Polem `strength: int`
        e. Polem `dexterity: int`
        f. Polem `constitution: int`
        g. Polem `intelligence: int`
        h. Polem `wisdom: int`
        i. Polem `charisma: int`
        j. Metodą `from_toml()`
    2. Metoda `from_toml()`:
        a. Parametr `filename: str`, przykład: 'myfile.toml'
        b. Parametr `name: str`, przykład: 'Sarevok'
        b. Zwraca instancję klasy na której została wykonana
    3. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * open(filename, mode='rb')
    * import tomllib
    * tomllib.load()

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from os import remove
    >>> from inspect import isclass

    >>> assert isclass(Character)
    >>> assert isclass(Fighter)
    >>> assert isclass(WildMage)
    >>> assert isclass(Ranger)
    >>> assert isclass(Thief)

    >>> imoen = Thief.from_toml(FILE, 'Imoen')
    >>> assert type(imoen.character_class) is str
    >>> assert type(imoen.race) is str
    >>> assert type(imoen.alignment) is str
    >>> assert type(imoen.stats) is Stats
    >>> assert imoen.character_class == 'Thief'
    >>> assert imoen.race == 'Human'
    >>> assert imoen.alignment == 'Neutral Good'
    >>> assert imoen.stats == Stats(strength=9, dexterity=18,
    ...                             constitution=16, intelligence=17,
    ...                             wisdom=11, charisma=16)

    >>> minsc = Ranger.from_toml(FILE, 'Minsc')
    >>> assert type(minsc.character_class) is str
    >>> assert type(minsc.race) is str
    >>> assert type(minsc.alignment) is str
    >>> assert type(minsc.stats) is Stats
    >>> assert minsc.character_class == 'Ranger'
    >>> assert minsc.race == 'Human'
    >>> assert minsc.alignment == 'Neutral Good'
    >>> assert minsc.stats == Stats(strength=18, dexterity=15,
    ...                             constitution=15, intelligence=8,
    ...                             wisdom=6, charisma=9)

    >>> neera = WildMage.from_toml(FILE, 'Neera')
    >>> assert type(neera.character_class) is str
    >>> assert type(neera.race) is str
    >>> assert type(neera.alignment) is str
    >>> assert type(neera.stats) is Stats
    >>> assert neera.character_class == 'Wild Mage'
    >>> assert neera.race == 'Half-elf'
    >>> assert neera.alignment == 'Chaotic Neutral'
    >>> assert neera.stats == Stats(strength=11, dexterity=17,
    ...                             constitution=14, intelligence=17,
    ...                             wisdom=10, charisma=11)

    >>> sarevok = Fighter.from_toml(FILE, 'Sarevok')
    >>> assert type(sarevok.character_class) is str
    >>> assert type(sarevok.race) is str
    >>> assert type(sarevok.alignment) is str
    >>> assert type(sarevok.stats) is Stats
    >>> assert sarevok.character_class == 'Fighter'
    >>> assert sarevok.race == 'Human'
    >>> assert sarevok.alignment == 'Chaotic Evil'
    >>> assert sarevok.stats == Stats(strength=18, dexterity=17,
    ...                               constitution=18, intelligence=17,
    ...                               wisdom=10, charisma=15)

    >>> remove(FILE)
"""
import tomllib
from dataclasses import dataclass
from pathlib import Path
from random import randint
from typing import NamedTuple

FILE = '_temporary.toml'

DATA = b"""
[Imoen]
character_class = 'Thief'
race = 'Human'
alignment = 'Neutral Good'
strength = 9
dexterity = 18
constitution = 16
intelligence = 17
wisdom = 11
charisma = 16

[Minsc]
character_class = 'Ranger'
race = 'Human'
alignment = 'Neutral Good'
strength = 18
dexterity = 15
constitution = 15
intelligence = 8
wisdom = 6
charisma = 9

[Neera]
character_class = 'Wild Mage'
race = 'Half-elf'
alignment = 'Chaotic Neutral'
strength = 11
dexterity = 17
constitution = 14
intelligence = 17
wisdom = 10
charisma = 11

[Sarevok]
character_class = 'Fighter'
race = 'Human'
alignment = 'Chaotic Evil'
strength = 18
dexterity = 17
constitution = 18
intelligence = 17
wisdom = 10
charisma = 15
"""

with open(FILE, mode='wb') as file:
    file.write(DATA)

class Stats(NamedTuple):
    strength: int
    dexterity: int
    constitution: int
    intelligence: int
    wisdom: int
    charisma: int


@dataclass
class Character:
    character_class: str
    race: str
    alignment: str
    strength: int
    dexterity: int
    constitution: int
    intelligence: int
    wisdom: int
    charisma: int

    @property
    def stats(self) -> Stats:
        return Stats(
            strength=self.strength,
            dexterity=self.dexterity,
            constitution=self.constitution,
            intelligence=self.intelligence,
            wisdom=self.wisdom,
            charisma=self.charisma)

    # parameter: `filename: str`, example: 'myfile.toml'
    # parameter: `name: str`, example: 'Sarevok'
    # hint: open file and read TOML content, parse it for given `name`
    # return: instance of a class on which was called
    # type: Callable[[type[Self], str, str], Self]
    def from_toml():
        ...


class Fighter(Character):
    pass


class WildMage(Character):
    pass


class Ranger(Character):
    pass


class Thief(Character):
    pass


Code 3.67. Solution
"""
* Assignment: OOP MethodClassmethod CSVMixin
* Complexity: medium
* Lines of code: 4 lines
* Time: 5 min

English:
    1. To class `CSVMixin` add methods:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> 'User' | 'Admin'`
    2. `CSVMixin.to_csv()` should return attribute values separated with coma
    3. `CSVMixin.from_csv()` should return instance of a class on which it was called
    4. Use `@classmethod` decorator in proper place
    5. Run doctests - all must succeed

Polish:
    1. Do klasy `CSVMixin` dodaj metody:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> 'User' | 'Admin'`
    2. `CSVMixin.to_csv()` powinna zwracać wartości atrybutów klasy rozdzielone po przecinku
    3. `CSVMixin.from_csv()` powinna zwracać instancje klasy na której została wywołana
    4. Użyj dekoratora `@classmethod` w odpowiednim miejscu
    5. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `CSVMixin.to_csv()` should add newline `\n` at the end of line
    * `CSVMixin.from_csv()` should remove newline `\n` at the end of line

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from os import remove

    >>> mark = User('Mark', 'Watney')
    >>> melissa = Admin('Melissa', 'Lewis')

    >>> mark.to_csv()
    'Mark,Watney\\n'
    >>> melissa.to_csv()
    'Melissa,Lewis\\n'

    >>> with open('_temporary.txt', mode='wt') as file:
    ...     data = mark.to_csv() + melissa.to_csv()
    ...     file.writelines(data)

    >>> result = []
    >>> with open('_temporary.txt', mode='rt') as file:
    ...     lines = file.readlines()
    ...     result += [User.from_csv(lines[0])]
    ...     result += [Admin.from_csv(lines[1])]

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [User(firstname='Mark', lastname='Watney'),
     Admin(firstname='Melissa', lastname='Lewis')]

    >>> remove('_temporary.txt')

TODO: dodać test sprawdzający czy linia kończy się newline
"""
from dataclasses import dataclass



class CSVMixin:

    # return: attribute values separated with coma
    # type: Callable[[Self], str]
    def to_csv(self) -> str:
        ...

    # return: instance of a class on which it was called
    # type: Callable[[type[Self], str], Self]
    @classmethod
    def from_csv(cls, line: str):
        ...


@dataclass
class User(CSVMixin):
    firstname: str
    lastname: str

@dataclass
class Admin(CSVMixin):
    firstname: str
    lastname: str