6.4. Classmethod

6.4.1. Rationale

  • Using class as namespace

  • Will pass class as a first argument

  • self is not required

6.4.2. Example

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
    lastname: str


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')
Code 6.110. 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
    lastname: str


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)
# Traceback (most recent call last):
# TypeError: 'User' object is not callable
Code 6.111. Trying to use method with self.__init__()
import json
from dataclasses import dataclass


class JSONMixin:
    def from_json(self, data):
        data = json.loads(data)
        return self.__init__(**data)


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


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'

result = User(None, None).from_json(DATA)
type(result)
# <class 'NoneType'>
Code 6.112. Trying to use staticmethod
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"}'

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')

6.4.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 User:
    firstname: str
    lastname: str


class Guest(User, JSONMixin):
    pass


class Admin(User, JSONMixin):
    pass


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

guest = Guest.from_json(DATA)
admin = Admin.from_json(DATA)

type(guest)     # <class '__main__.Guest'>
type(admin)     # <class '__main__.Admin'>

print(guest)    # Guest(firstname='Jan', lastname='Twardowski')
print(admin)    # Admin(firstname='Jan', lastname='Twardowski')
class AbstractTime:
    tzname: str
    tzcode: str

    @classmethod
    def parse(cls, text):
        result = ...
        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

6.4.4. Assignments

Code 6.113. Solution
"""
* Assignment: Protocol Classmethod CSV
* Filename: protocol_classmethod_csv.py
* Complexity: easy
* Lines of code to write: 5 lines
* Estimated time: 13 min

English:
    1. Use data from "Given" section (see below)
    2. To class `CSVMixin` add methods:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> Union['Astronaut', 'Cosmonaut']`
    3. `CSVMixin.to_csv()` should return attribute values separated with coma
    4. `CSVMixin.from_csv()` should return instance of a class on which it was called
    5. Use `@classmethod` decorator in proper place
    6. All tests must pass
    7. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Do klasy `CSVMixin` dodaj metody:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> Union['Astronaut', 'Cosmonaut']`
    3. `CSVMixin.to_csv()` powinna zwracać wartości atrybutów klasy rozdzielone po przecinku
    4. `CSVMixin.from_csv()` powinna zwracać instancje klasy na której została wywołana
    5. Użyj dekoratora `@classmethod` w odpowiednim miejscu
    6. Wszystkie testy muszą przejść
    7. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

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

Tests:
    >>> from dataclasses import dataclass

    >>> @dataclass
    ... class Human:
    ...     firstname: str
    ...     lastname: str

    >>> class Astronaut(Human, CSVMixin):
    ...     pass
    ...

    >>> class Cosmonaut(Human, CSVMixin):
    ...     pass

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

    >>> with open('_temporary.txt', mode='wt') as file:
    ...    file.writelines(csv)

    >>> 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')]
"""

# Given
from typing import Union


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

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