5.5. Protocol Classmethod

5.5.1. Rationale

  • Using class as namespace

  • Will pass class as a first argument

  • self is not required

>>> class MyClass:
...     def mymethod(self):
...         pass
>>> class MyClass:
...     @staticmethod
...     def mymethod():
...         pass
>>> class MyClass:
...     @classmethod
...     def mymethod(cls):
...         pass

5.5.2. Example

>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...
...     def from_json(self, data):
...         data = json.loads(data)
...         return User(**data)
>>>
>>>
>>> DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'
>>>
>>> User.from_json(DATA)
Traceback (most recent call last):
TypeError: from_json() missing 1 required positional argument: 'data'
>>>
>>> User().from_json(DATA)
Traceback (most recent call last):
TypeError: __init__() missing 2 required positional arguments: 'firstname' and 'lastname'
>>>
>>> User(None, None).from_json(DATA)
User(firstname='Jan', lastname='Twardowski')
>>> 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": "Jan", "lastname": "Twardowski"}'
>>>
>>> User.from_json(DATA)
User(firstname='Jan', lastname='Twardowski')
>>> 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": "Jan", "lastname": "Twardowski"}'
>>>
>>> print(User.from_json(DATA))
User(firstname='Jan', lastname='Twardowski')
>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> class JSONMixin:
...     def from_json(self, data):
...         data = json.loads(data)
...         return User(**data)
>>>
>>>
>>> @dataclass
... class User(JSONMixin):
...     firstname: str = None
...     lastname: str = None
>>>
>>>
>>> DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'
>>>
>>> User.from_json(DATA)
Traceback (most recent call last):
TypeError: from_json() missing 1 required positional argument: 'data'
>>>
>>> User().from_json(DATA)
User(firstname='Jan', lastname='Twardowski')

Trying to use method with self:

>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> class JSONMixin:
...     def from_json(self, data):
...         data = json.loads(data)
...         return self(**data)
>>>
>>>
>>> @dataclass
... class User(JSONMixin):
...     firstname: str = None
...     lastname: str = None
>>>
>>>
>>> DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'
>>>
>>> User.from_json(DATA)
Traceback (most recent call last):
TypeError: from_json() missing 1 required positional argument: 'data'
>>>
>>> User().from_json(DATA)
Traceback (most recent call last):
TypeError: 'User' object is not callable

Trying to use method with self.__init__():

>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> class JSONMixin:
...     def from_json(self, data):
...         data = json.loads(data)
...         self.__init__(**data)
...         return self
>>>
>>>
>>> @dataclass
... class User(JSONMixin):
...     firstname: str = None
...     lastname: str = None
>>>
>>>
>>> DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'
>>>
>>> User.from_json(DATA)
Traceback (most recent call last):
TypeError: from_json() missing 1 required positional argument: 'data'
>>>
>>> User().from_json(DATA)
User(firstname='Jan', lastname='Twardowski')

Trying to use methods self.__new__() and self.__init__():

>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> class JSONMixin:
...     def from_json(self, data):
...         data = json.loads(data)
...         instance = object.__new__(type(self))
...         instance.__init__(**data)
...         return instance
>>>
>>>
>>> @dataclass
... class User(JSONMixin):
...     firstname: str = None
...     lastname: str = None
>>>
>>>
>>> DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'
>>>
>>> User.from_json(DATA)
Traceback (most recent call last):
TypeError: from_json() missing 1 required positional argument: 'data'
>>>
>>> User().from_json(DATA)
User(firstname='Jan', lastname='Twardowski')
>>> 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": "Jan", "lastname": "Twardowski"}'
>>>
>>> User.from_json(DATA)
User(firstname='Jan', lastname='Twardowski')

5.5.3. Use Cases

>>> import json
>>> from dataclasses import dataclass
>>>
>>>
>>> class JSONMixin:
...     @classmethod
...     def from_json(cls, data):
...         data = json.loads(data)
...         return cls(**data)
>>>
>>> @dataclass
... class Guest(JSONMixin):
...     firstname: str
...     lastname: str
>>>
>>> @dataclass
... class Admin(JSONMixin):
...     firstname: str
...     lastname: str
>>>
>>>
>>> DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'
>>>
>>> Guest.from_json(DATA)
Guest(firstname='Jan', lastname='Twardowski')
>>>
>>> Admin.from_json(DATA)
Admin(firstname='Jan', lastname='Twardowski')
>>> class AbstractTime:
...     tzname: str
...     tzcode: str
...
...     def __init__(self, date, time):
...         ...
...
...     @classmethod
...     def parse(cls, text):
...         result = {'date': ..., 'time': ...}
...         return cls(**result)
>>>
>>> class MartianTime(AbstractTime):
...     tzname = 'Coordinated Mars Time'
...     tzcode = 'MTC'
>>>
>>> class LunarTime(AbstractTime):
...     tzname = 'Lunar Standard Time'
...     tzcode = 'LST'
>>>
>>> class EarthTime(AbstractTime):
...     tzname = 'Universal Time Coordinated'
...     tzcode = 'UTC'
>>>
>>>
>>> # Settings
>>> time = MartianTime
>>>
>>> # Usage
>>> from settings import time  
>>>
>>> UTC = '1969-07-21T02:53:07Z'
>>>
>>> dt = time.parse(UTC)
>>> print(dt.tzname)
Coordinated Mars Time

5.5.4. Assignments

Code 5.52. Solution
"""
* Assignment: Protocol Classmethod CSV
* Complexity: easy
* Lines of code: 5 lines
* Time: 13 min

English:
    1. To class `CSVMixin` add methods:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> Union['Astronaut', 'Cosmonaut']`
    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. All tests must pass
    6. Run doctests - all must succeed

Polish:
    1. Do klasy `CSVMixin` dodaj metody:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> Union['Astronaut', 'Cosmonaut']`
    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. Wszystkie testy muszą przejść
    6. 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 dataclasses import dataclass

    >>> @dataclass
    ... class Astronaut(CSVMixin):
    ...     firstname: str
    ...     lastname: str
    ...
    >>> @dataclass
    ... class Cosmonaut(CSVMixin):
    ...     firstname: str
    ...     lastname: str

    >>> mark = Astronaut('Mark', 'Watney')
    >>> jan = Cosmonaut('Jan', 'Twardowski')
    >>> mark.to_csv()
    'Mark,Watney\\n'
    >>> jan.to_csv()
    'Jan,Twardowski\\n'

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

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

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [Astronaut(firstname='Mark', lastname='Watney'),
     Cosmonaut(firstname='Jan', lastname='Twardowski')]
    >>> from os import remove
    >>> remove('_temporary.txt')
"""

from typing import Union


class CSVMixin:
    def to_csv(self) -> str:
        ...

    @classmethod
    def from_csv(cls, line: str) -> Union['Astronaut', 'Cosmonaut']:
        ...