Repository: FM-17/poglink (language: python)

Issue:
Bans cog should make use of bans models for consistency.
Bans models were developed to standardize the way ban information is handled. These models should be employed within the cog responsible for handling these notifications.
travis packaging
Updates some packaging things. `setup.cfg` is now used for static info while `setup.py` is still maintained for dynamic content (as distinguished [here](https://packaging.python.org/en/latest/tutorials/packaging-projects/#configuring-metadata)).

I've switched from `python setup.py sdist` to a more generic `python -m build` as recommended [here](https://packaging.python.org/en/latest/tutorials/packaging-projects/#generating-distribution-archives), which reads the `pyproject.toml` file to determine which build backend to use (still `setuptools` in our case).

Also of note, I added a license file as it's highly recommended to use. Given that you've made this public and shared it, I think MIT license is most appropriate. [Here](https://choosealicense.com/licenses/mit/#) is some more info if you'd like to consider this further and possibly choose a different one.

Lastly, I added `isort` to the Makefile to help keep code tidy and consistent. No CI enforcement, but I could add that too if you'd like. **EDIT**: I added it to CI.

Some helpful links:
- https://towardsdatascience.com/packaging-in-python-tools-and-formats-743ead5f39ee
- https://choosealicense.com/
- https://packaging.python.org/en/latest/tutorials/packaging-projects/#packaging-python-projects
- https://setuptools.pypa.io/en/latest/userguide/quickstart.html


Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index 9c71811..b4c4ca5 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,11 @@ all: help
 .PHONY: format 
 format: isort lint
 
+## - format-check
+##	Check (but don't apply) black linting and isort sorting.
+.PHONY: format-check 
+format-check: lint-check isort-check
+
 ## - lint
 ##	Automatically format project code with black.
 .PHONY: lint 
@@ -16,7 +21,7 @@ lint:
 ##	Check linting of code with black.
 .PHONY: lint-check 
 lint-check:
-	black poglink tests --diff --color
+	black poglink tests --diff --color --check
 
 ## - isort
 ##	Automatically sort imports in project code using isort.
@@ -28,7 +33,7 @@ isort:
 ##	Check sorting of imports in project code using isort.
 .PHONY: isort-check 
 isort-check:
-	isort poglink tests --diff --color
+	isort poglink tests --diff --color --check
 
 ## - test
 ##	Run pytest tests.
diff --git a/TODO.md b/TODO.md
index ae5378d..9cd40d5 100644
--- a/TODO.md
+++ b/TODO.md
@@ -27,6 +27,6 @@ server
 - [ ] Add auto-publish/no publish as a parameter for each cog, perhaps `auto-publish-channels = [<chan_ID1>, <chan_ID1>]`
 - [ ] In-game server notifications posted to Discord channels
 - [x] Convert to use `setup.cfg` instead of `setup.py`
-- [ ] Add `old_rates` and `current_rates` to `RatesDiff` so `self.current_rates` could be used instead of `to_embed` 
-- [ ] Allow for `old_rates=old` and `new_rates=new` to be passed into `RatesStatus.to_diff()`  
+- [x] Add `old_rates` and `current_rates` to `RatesDiff` so `self.current_rates` could be used instead of `to_embed` 
+- [x] Allow for `old_rates=old` and `new_rates=new` to be passed into `RatesStatus.to_diff()`  
 - [ ] Add ability to provide custom rates URL
diff --git a/poglink/cogs/rates.py b/poglink/cogs/rates.py
index abf898e..6395a82 100644
--- a/poglink/cogs/rates.py
+++ b/poglink/cogs/rates.py
@@ -143,7 +143,7 @@ async def on_ready(self):
                 last_rates = RatesStatus.from_dict(last_rates_dict)
 
                 # compare rates to last rates
-                rates_diff = rates.get_diff(last_rates)
+                rates_diff = last_rates.get_diff(rates)
 
                 if rates_diff.items:
 
@@ -158,7 +158,7 @@ async def on_ready(self):
 
                     # generate and send embed
                     logger.info(f"Rates at {url} changed - sending embed")
-                    embed_description = rates_diff.to_embed(rates)
+                    embed_description = rates_diff.to_embed()
                     await self.send_embed(embed_description, url)
 
             await asyncio.sleep(self.polling_delay)
diff --git a/poglink/models.py b/poglink/models.py
deleted file mode 100644
index 347f38d..0000000
--- a/poglink/models.py
+++ /dev/null
@@ -1,256 +0,0 @@
-import datetime
-import re
-from dataclasses import dataclass, field
-from typing import Any, List, Tuple
-
-from dateutil import parser as dateparser
-from jinja2 import Template
-
-
-class RatesStatus:
-
-    RATES_NAMES = {
-        "TamingSpeedMultiplier": "Taming",
-        "HarvestAmountMultiplier": "Harvesting",
-        "XPMultiplier": "XP",
-        "MatingIntervalMultiplier": "Mating Interval",
-        "BabyMatureSpeedMultiplier": "Maturation",
-        "EggHatchSpeedMultiplier": "Hatching",
-        "BabyCuddleIntervalMultiplier": "Cuddle Interval",
-        "BabyImprintAmountMultiplier": "Imprinting",
-        "HexagonRewardMultiplier": "Hexagon Reward",
-    }
-
-    def __init__(
-        self,
-        **kwargs,
-    ):
-        expected, extras = self.get_expected_and_extras(kwargs)
-
-        for k, v in expected.items():
-            setattr(self, k, v)
-
-        self.extras = extras
-
-    def __eq__(self, __o: object) -> bool:
-        return self.__dict__ == __o.__dict__
-
-    @staticmethod
-    def parse_raw(raw: str) -> dict:
-        parsed_vals = {
-            key.strip(): val.strip()
-            for key, val in [line.split("=") for line in raw.strip().splitlines()]
-        }
-        return parsed_vals
-
-    @staticmethod
-    def get_expected_and_extras(parsed_vals_dict: dict) -> Tuple[dict, dict]:
-        expected = {
-            k: v for k, v in parsed_vals_dict.items() if k in RatesStatus.RATES_NAMES
-        }
-        extras = {
-            k: v
-            for k, v in parsed_vals_dict.items()
-            if k not in RatesStatus.RATES_NAMES and k != "extras"
-        }
-
-        if "extras" in parsed_vals_dict:
-            extras.update(parsed_vals_dict.get("extras"))
-
-        return expected, extras
-
-    @classmethod
-    def from_raw(cls, raw_txt: str):
-        val_dict = cls.parse_raw(raw_txt)
-
-        return cls.from_dict(val_dict)
-
-    @classmethod
-    def from_dict(cls, val_dict):
-        expected, extras = cls.get_expected_and_extras(val_dict)
-
-        return cls(**expected, **extras)
-
-    def update(self, vals, raw=False):
-
-        parsed_vals = self.parse_raw(vals) if raw else vals
-
-        expected, extras = self.get_expected_and_extras(parsed_vals)
-
-        for k, v in expected.items():
-            setattr(self, k, v)
-
-        self.extras.update(extras)
-
-    def to_dict(self):
-        expected, extras = self.get_expected_and_extras(self.__dict__)
-
-        output_dict = expected
-        output_dict.update(extras)
-
-        return output_dict
-
-    def to_raw(self):
-        return "\n".join([f"{k}={v}" for k, v in self.to_dict().items()])
-
-    def get_diff(self, newrates: "RatesStatus"):
-        old = self.to_dict()
-        new = newrates.to_dict()
-
-        return RatesDiff(
-            items=sorted(
-                [
-                    RatesDiffItem(
-                        key=k,
-                        old=old.get(k),
-                        new=new.get(k),
-                        extra=k not in RatesStatus.RATES_NAMES,
-                    )
-                    for k, _ in set(new.items()) - set(old.items())
-                ],
-                key=lambda x: x.key,
-            )
-        )
-
-
-@dataclass
-class BansPlatformPair:
-    platform: str
-    nbans: int
-
-    def to_dict(self):
-        return {self.platform: self.nbans}
-
-
-@dataclass
-class BansTimePeriodSummary:
-    heading: str
-    summary: List[BansPlatformPair]
-
-    def to_dict(self):
-        return {self.heading: [s.to_dict() for s in self.summary]}
-
-
-class BansStatus:
-    def __init__(
-        self,
-        bans: List[BansTimePeriodSummary],
-        last_updated: datetime.datetime = None,
-    ) -> None:
-        self.bans = bans
-        self.last_updated = (
-            dateparser.parse(last_updated)
-            if isinstance(last_updated, str)
-            else last_updated
-        )
-
-    def __eq__(self, __o: object) -> bool:
-        return self.__dict__ == __o.__dict__
-
-    @staticmethod
-    def parse_raw(raw_txt):
-        # https://regex101.com/r/KCBt9M/1
-        matches = re.findall("(.*)\n(?:=+\n)((?:.+\n)+)", raw_txt)
-
-        parsed_dict = {
-            heading: {
-                platform.strip(): int(nbans)
-                for platform, nbans in [
-                    line.split(":") for line in summary.splitlines()
-                ]
-            }
-            for heading, summary in matches
-        }
-
-        # TODO: Handle if format ever changes
-        date_string = re.search(r"Last Updated: (.*)\s.+", raw_txt).group(1)
-        last_updated = dateparser.parse(date_string)
-        return parsed_dict, last_updated
-
-    @classmethod
-    def from_raw(cls, raw_txt):
-        status_dict, last_updated = cls.parse_raw(raw_txt)
-
-        return cls.from_dict(
-            {"ban_summaries": status_dict, "last_updated": last_updated}
-        )
-
-    def to_dict(self):
-        output_dict = {
-            "last_updated": self.last_updated.isoformat(),
-            "ban_summaries": {
-                b.heading: {s.platform: s.nbans for s in b.summary} for b in self.bans
-            },
-        }
-        return output_dict
-
-    def to_raw(self):
-        return self.ban_summary_template.render(banstatus=self)
-
-    @classmethod
-    def from_dict(cls, d):
-        status_dict = d.get("ban_summaries")
-        last_updated = d.get("last_updated")
-        return cls(
-            bans=[
-                BansTimePeriodSummary(
-                    heading=period,
-                    summary=[
-                        BansPlatformPair(platform=p, nbans=n)
-                        for p, n in summary.items()
-                    ],
-                )
-                for period, summary in status_dict.items()
-            ],
-            last_updated=last_updated,
-        )
-
-    @staticmethod
-    def underline(length):
-        return "=" * length
-
-    ban_summary_template = Template(
-        """{%- for ban_timeperiod in banstatus.bans %}
-{{- ban_timeperiod.heading }}
-{% for _ in range(ban_timeperiod.heading | length) %}={% endfor %}
-{% for ban_summary in ban_timeperiod.summary %}
-{{- ban_summary.platform }}: {{ ban_summary.nbans}}
-{% endfor %}
-
-{% endfor -%}
-Last Updated: {{ banstatus.last_updated.strftime('%d %b %Y %H:%M:%S') }} ET 
-"""
-    )
-
-
-@dataclass
-class RatesDiffItem:
-    key: str
-    old: Any
-    new: Any
-    extra: bool = False
-
-
-@dataclass
-class RatesDiff:
-    items: List[RatesDiffItem] = field(default_factory=list)
-
-    # highlights changed rates in embed
-    def to_embed(self, rates):
-
-        rates_dict = rates.to_dict()
-
-        # bold updated rates
-        updated_rates = {item.key: f"**{item.new}**" for item in self.items}
-
-        rates_dict.update(updated_rates)
-
-        # rename keys and format embed description
-        embed_description = "\n".join(
-            [
-                f"{v.rstrip('.0')} × {rates.RATES_NAMES.get(k,k)}"
-                for k, v in rates_dict.items()
-            ]
-        )
-
-        return embed_description
diff --git a/poglink/models/__init__.py b/poglink/models/__init__.py
new file mode 100644
index 0000000..2febf3b
--- /dev/null
+++ b/poglink/models/__init__.py
@@ -0,0 +1,2 @@
+from .bans import BansPlatformPair, BansStatus, BansTimePeriodSummary
+from .rates import RatesDiff, RatesDiffItem, RatesStatus
diff --git a/poglink/models/bans.py b/poglink/models/bans.py
new file mode 100644
index 0000000..d779ed1
--- /dev/null
+++ b/poglink/models/bans.py
@@ -0,0 +1,117 @@
+import datetime
+import re
+from dataclasses import dataclass
+from typing import List
+
+from dateutil import parser as dateparser
+from jinja2 import Template
+
+
+@dataclass
+class BansPlatformPair:
+    platform: str
+    nbans: int
+
+    def to_dict(self):
+        return {self.platform: self.nbans}
+
+
+@dataclass
+class BansTimePeriodSummary:
+    heading: str
+    summary: List[BansPlatformPair]
+
+    def to_dict(self):
+        return {self.heading: [s.to_dict() for s in self.summary]}
+
+
+class BansStatus:
+    def __init__(
+        self,
+        bans: List[BansTimePeriodSummary],
+        last_updated: datetime.datetime = None,
+    ) -> None:
+        self.bans = bans
+        self.last_updated = (
+            dateparser.parse(last_updated)
+            if isinstance(last_updated, str)
+            else last_updated
+        )
+
+    def __eq__(self, __o: object) -> bool:
+        return self.__dict__ == __o.__dict__
+
+    @staticmethod
+    def parse_raw(raw_txt):
+        # https://regex101.com/r/KCBt9M/1
+        matches = re.findall("(.*)\n(?:=+\n)((?:.+\n)+)", raw_txt)
+
+        parsed_dict = {
+            heading: {
+                platform.strip(): int(nbans)
+                for platform, nbans in [
+                    line.split(":") for line in summary.splitlines()
+                ]
+            }
+            for heading, summary in matches
+        }
+
+        # TODO: Handle if format ever changes
+        date_string = re.search(r"Last Updated: (.*)\s.+", raw_txt).group(1)
+        last_updated = dateparser.parse(date_string)
+        return parsed_dict, last_updated
+
+    @classmethod
+    def from_raw(cls, raw_txt):
+        status_dict, last_updated = cls.parse_raw(raw_txt)
+
+        return cls.from_dict(
+            {"ban_summaries": status_dict, "last_updated": last_updated}
+        )
+
+    def to_dict(self):
+        output_dict = {
+            "last_updated": self.last_updated.isoformat(),
+            "ban_summaries": {
+                b.heading: {s.platform: s.nbans for s in b.summary} for b in self.bans
+            },
+        }
+        return output_dict
+
+    def to_raw(self):
+        return self.ban_summary_template.render(banstatus=self)
+
+    @classmethod
+    def from_dict(cls, d):
+        status_dict = d.get("ban_summaries")
+        last_updated = d.get("last_updated")
+        return cls(
+            bans=[
+                BansTimePeriodSummary(
+                    heading=period,
+                    summary=[
+                        BansPlatformPair(platform=p, nbans=n)
+                        for p, n in summary.items()
+                    ],
+                )
+                for period, summary in status_dict.items()
+            ],
+            last_updated=last_updated,
+        )
+
+    @staticmethod
+    def underline(length):
+        return "=" * length
+
+    ban_summary_template = Template(
+        """{%- for ban_timeperiod in banstatus.bans %}
+{{- ban_timeperiod.heading }}
+{% for _ in range(ban_timeperiod.heading | length) %}={% endfor %}
+{% for ban_summary in ban_timeperiod.summary %}
+{{- ban_summary.platform }}: {{ ban_summary.nbans}}
+{% endfor %}
+
+{% endfor -%}items: List[RatesDiffItem] = field(default_factory=list)
+Last Updated: {{ banstatus.last_updated.strftime('%d %b %Y %H:%M:%S') }} ET 
+"""
+    )
diff --git a/poglink/models/rates.py b/poglink/models/rates.py
new file mode 100644
index 0000000..ec193d7
--- /dev/null
+++ b/poglink/models/rates.py
@@ -0,0 +1,149 @@
+from dataclasses import dataclass, field
+from typing import Any, List, Tuple
+
+
+class RatesStatus:
+
+    RATES_NAMES = {
+        "TamingSpeedMultiplier": "Taming",
+        "HarvestAmountMultiplier": "Harvesting",
+        "XPMultiplier": "XP",
+        "MatingIntervalMultiplier": "Mating Interval",
+        "BabyMatureSpeedMultiplier": "Maturation",
+        "EggHatchSpeedMultiplier": "Hatching",
+        "BabyCuddleIntervalMultiplier": "Cuddle Interval",
+        "BabyImprintAmountMultiplier": "Imprinting",
+        "HexagonRewardMultiplier": "Hexagon Reward",
+    }
+
+    def __init__(
+        self,
+        **kwargs,
+    ):
+        expected, extras = self.get_expected_and_extras(kwargs)
+
+        for k, v in expected.items():
+            setattr(self, k, v)
+
+        self.extras = extras
+
+    def __eq__(self, __o: object) -> bool:
+        return self.__dict__ == __o.__dict__
+
+    @staticmethod
+    def parse_raw(raw: str) -> dict:
+        parsed_vals = {
+            key.strip(): val.strip()
+            for key, val in [line.split("=") for line in raw.strip().splitlines()]
+        }
+        return parsed_vals
+
+    @staticmethod
+    def get_expected_and_extras(parsed_vals_dict: dict) -> Tuple[dict, dict]:
+        expected = {
+            k: v for k, v in parsed_vals_dict.items() if k in RatesStatus.RATES_NAMES
+        }
+        extras = {
+            k: v
+            for k, v in parsed_vals_dict.items()
+            if k not in RatesStatus.RATES_NAMES and k != "extras"
+        }
+
+        if "extras" in parsed_vals_dict:
+            extras.update(parsed_vals_dict.get("extras"))
+
+        return expected, extras
+
+    @classmethod
+    def from_raw(cls, raw_txt: str):
+        val_dict = cls.parse_raw(raw_txt)
+
+        return cls.from_dict(val_dict)
+
+    @classmethod
+    def from_dict(cls, val_dict):
+        expected, extras = cls.get_expected_and_extras(val_dict)
+
+        return cls(**expected, **extras)
+
+    def update_vals(self, vals, raw=False):
+
+        parsed_vals = self.parse_raw(vals) if raw else vals
+
+        expected, extras = self.get_expected_and_extras(parsed_vals)
+
+        for k, v in expected.items():
+            setattr(self, k, v)
+
+        self.extras.update(extras)
+
+    def to_dict(self):
+        expected, extras = self.get_expected_and_extras(self.__dict__)
+
+        output_dict = expected
+        output_dict.update(extras)
+
+        return output_dict
+
+    def to_raw(self):
+        return "\n".join([f"{k}={v}" for k, v in self.to_dict().items()])
+
+    def get_diff(self, newrates: "RatesStatus"):
+        return RatesDiff.from_statuses(self, newrates)
+
+
+@dataclass
+class RatesDiffItem:
+    key: str
+    old_val: Any
+    new_val: Any
+    is_extra: bool = False
+
+
+@dataclass
+class RatesDiff:
+    old: RatesStatus = field(default_factory=RatesStatus)
+    new: RatesStatus = field(default_factory=RatesStatus)
+    items: List[RatesDiffItem] = field(default_factory=list)
+
+    @classmethod
+    def from_statuses(cls, old, new):
+        old_dict = old.to_dict()
+        new_dict = new.to_dict()
+
+        return RatesDiff(
+            items=sorted(
+                [
+                    RatesDiffItem(
+                        key=k,
+                        old_val=old_dict.get(k),
+                        new_val=new_dict.get(k),
+                        is_extra=k not in RatesStatus.RATES_NAMES,
+                    )
+                    for k, _ in set(new_dict.items()) - set(old_dict.items())
+                ],
+                key=lambda x: x.key,
+            ),
+            old=old,
+            new=new,
+        )
+
+    # highlights changed rates in embed
+    def to_embed(self):
+
+        rates_dict = self.old.to_dict()
+
+        # bold updated rates
+        updated_rates = {item.key: f"**{item.new_val}**" for item in self.items}
+
+        rates_dict.update(updated_rates)
+
+        # rename keys and format embed description
+        embed_description = "\n".join(
+            [
+                f"{v.rstrip('.0')} × {self.old.RATES_NAMES.get(k,k)}"  # TODO: rstrip won't remove '.0' if wrapped in ** from above
+                for k, v in rates_dict.items()
+            ]
+        )
+
+        return embed_description
diff --git a/requirements.dev.txt b/requirements.dev.txt
index 287afd6..026e873 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -1,5 +1,6 @@
 black==20.8b1
 build==0.7.0
+colorama==0.4.4
 isort==5.10.1
 pytest-asyncio==0.16.0
 pytest-httpserver==1.0.2


Test patch:
diff --git a/tests/unit/test_models_rates.py b/tests/unit/test_models_rates.py
index d9afd0c..356b4ee 100644
--- a/tests/unit/test_models_rates.py
+++ b/tests/unit/test_models_rates.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from poglink.models import RatesDiff, RatesDiffItem, RatesStatus
+from poglink.models import RatesDiff, RatesDiffItem, RatesStatus, rates
 
 
 @pytest.fixture()
@@ -69,49 +69,49 @@ def rates_update_dict():
     }
 
 
-def test_parse_rates(sample_rates_txt):
+def test_rates_staticmethod_parse_raw(sample_rates_txt):
     d = RatesStatus.parse_raw(sample_rates_txt)
 
     assert d.get("TamingSpeedMultiplier") == "3.0"
     assert d.get("HexagonRewardMultiplier") == "1.5"
 
 
-def test_from_raw(sample_rates_txt):
+def test_rates_classmethod_from_raw(sample_rates_txt):
     rates = RatesStatus.from_raw(sample_rates_txt)
     assert rates.MatingIntervalMultiplier == "0.6"
     assert rates.BabyCuddleIntervalMultiplier == "0.6"
     assert rates.extras == {"MyMadeUpValue": "69420"}
 
 
-def test_from_dict(sample_rates_dict):
+def test_rates_classmethod_from_dict(sample_rates_dict):
     rates = RatesStatus.from_dict(sample_rates_dict)
     assert rates.MatingIntervalMultiplier == "0.6"
     assert rates.BabyCuddleIntervalMultiplier == "0.6"
     assert rates.extras == {"MyMadeUpValue": "69420"}
 
 
-def test_update_from_raw(sample_rates_txt, rates_update_txt):
+def test_rates_method_update_raw(sample_rates_txt, rates_update_txt):
     rates = RatesStatus.from_raw(sample_rates_txt)
     assert rates.XPMultiplier == "3.0"
     assert rates.extras == {"MyMadeUpValue": "69420"}
 
-    rates.update(rates_update_txt, raw=True)
+    rates.update_vals(rates_update_txt, raw=True)
 
     assert rates.XPMultiplier == "2.0"
     assert rates.extras == {"MyMadeUpValue": "42069", "MyOtherMadeUpValue": "42069"}
 
 
-def test_update_from_dict(sample_rates_dict, rates_update_dict):
+def test_rates_method_update_dict(sample_rates_dict, rates_update_dict):
     rates = RatesStatus.from_dict(sample_rates_dict)
     assert rates.XPMultiplier == "3.0"
 
-    rates.update(rates_update_dict, raw=False)
+    rates.update_vals(rates_update_dict, raw=False)
 
     assert rates.XPMultiplier == "2.0"
     assert rates.extras == {"MyMadeUpValue": "42069", "MyOtherMadeUpValue": "42069"}
 
 
-def test_get_expected_and_extras(sample_rates_dict):
+def test_rates_staticmethod_get_expected_and_extras(sample_rates_dict):
     expected, extras = RatesStatus.get_expected_and_extras(sample_rates_dict)
 
     assert expected == {
@@ -131,19 +131,19 @@ def test_get_expected_and_extras(sample_rates_dict):
     }
 
 
-def test_to_raw(sample_rates_dict, sample_rates_txt):
+def test_rates_method_to_raw(sample_rates_dict, sample_rates_txt):
     rates = RatesStatus.from_dict(sample_rates_dict)
 
     assert rates.to_raw() == sample_rates_txt
 
 
-def test_to_dict(sample_rates_dict, sample_rates_txt):
+def test_rates_method_to_dict(sample_rates_dict, sample_rates_txt):
     rates = RatesStatus.from_raw(sample_rates_txt)
 
     assert rates.to_dict() == sample_rates_dict
 
 
-def test_get_diff(sample_rates_dict):
+def test_rates_method_get_diff(sample_rates_dict):
     rates = RatesStatus.from_dict(sample_rates_dict)
 
     sample_rates_dict["BabyMatureSpeedMultiplier"] = "4.2"
@@ -156,22 +156,53 @@ def test_get_diff(sample_rates_dict):
     assert diff == RatesDiff(
         items=[
             RatesDiffItem(
-                key="BabyMatureSpeedMultiplier", old="3.0", new="4.2", extra=False
+                key="BabyMatureSpeedMultiplier",
+                old_val="3.0",
+                new_val="4.2",
+                is_extra=False,
             ),
             RatesDiffItem(
-                key="CompletelyRandomNewThing", old=None, new="2.2", extra=True
+                key="CompletelyRandomNewThing",
+                old_val=None,
+                new_val="2.2",
+                is_extra=True,
             ),
-        ]
+        ],
+        old=rates,
+        new=newrates,
     )
 
     rates_copy = copy.deepcopy(rates)
     diff = rates.get_diff(rates_copy)
 
-    assert diff == RatesDiff()
+    assert diff == RatesDiff(old=rates, new=rates_copy)
 
 
-def test_rates_comparison(sample_rates_dict, sample_rates_txt):
+def test_rates_eq(sample_rates_dict, sample_rates_txt):
     rates_from_dict = RatesStatus.from_dict(sample_rates_dict)
     rates_from_txt = RatesStatus.from_raw(sample_rates_txt)
 
     assert rates_from_dict == rates_from_txt
+
+
+def test_rates_method_to_embed(sample_rates_dict):
+    old_rates = RatesStatus.from_dict(sample_rates_dict)
+    sample_rates_dict["XPMultiplier"] = 1.0
+    sample_rates_dict["MatingIntervalMultiplier"] = 1.0
+    new_rates = RatesStatus.from_dict(sample_rates_dict)
+
+    rates_diff = RatesDiff.from_statuses(old=old_rates, new=new_rates)
+
+    highlighted_embed = """\
+3 × Taming
+3 × Harvesting
+**1.0** × XP
+**1.0** × Mating Interval
+3 × Maturation
+3 × Hatching
+0.6 × Cuddle Interval
+3 × Imprinting
+1.5 × Hexagon Reward
+6942 × MyMadeUpValue\
+"""
+    assert rates_diff.to_embed() == highlighted_embed