From c0dcf260f42b5219c30aab557a3056636f3e8d7b Mon Sep 17 00:00:00 2001 From: asquator Date: Sun, 22 Dec 2024 10:38:21 +0200 Subject: [PATCH 01/13] Support for nox-testing single modules using session arguments --- noxfile.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/noxfile.py b/noxfile.py index 4233570..24a3825 100644 --- a/noxfile.py +++ b/noxfile.py @@ -30,8 +30,9 @@ def test( ) -> None: session.debug("Running The tests.") junit_file: Path = _PROJECT_ROOT / "junit.xml" - session.run( - # Added python -m as it did problems on windows. + + # Prepare the command to run pytest + command = [ *get_python_prefix(), "coverage", "run", @@ -39,15 +40,21 @@ def test( f"--source={_CODE_FILES!s}", "-m", "pytest", - f"{_PROJECT_ROOT!s}", "--full-trace", "--showlocals", "--show-capture=all", "-n", "auto", f"--junit-xml={junit_file!s}", - ) - session.debug("Ran The tests.") + ] + + # Check for additional arguments (test files) + if session.posargs: + command.extend(session.posargs) # Add specific test files if provided + + # Run the command + session.run(*command) + @nox.session(tags=["test", "ci"]) From 6d65575ea713b0405d22a7dc40b47b1da7829dce Mon Sep 17 00:00:00 2001 From: asquator Date: Sun, 22 Dec 2024 11:01:11 +0200 Subject: [PATCH 02/13] Added property initialization and power validation --- tlab/air_bender.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tlab/air_bender.py b/tlab/air_bender.py index a910e62..72bed7c 100644 --- a/tlab/air_bender.py +++ b/tlab/air_bender.py @@ -1,5 +1,7 @@ from typing import Self +from tlab.exceptions import InvalidPowerValueError, InvalidPowerTypeError + class AirBender: # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations @@ -8,33 +10,38 @@ def __init__( name: str, power: int, ) -> None: - raise NotImplementedError("You Should Implement this method") + + self._validate_power(power) + + self._name = name + self._power = power @property def name( self: Self, ) -> str: - raise NotImplementedError("You Should Implement this method") + return self._name @name.setter def name( self: Self, name: str, ) -> None: - raise NotImplementedError("You Should Implement this method") + self._name = name @property def power( self: Self, ) -> int: - raise NotImplementedError("You Should Implement this method") + return self._power @power.setter def power( self: Self, power: int, ) -> None: - raise NotImplementedError("You Should Implement this method") + self._validate_power(power) + self._power = power @property def skill( @@ -46,3 +53,8 @@ def bend( self: Self, ) -> None: raise NotImplementedError("You Should Implement this method") + + @classmethod + def _validate_power(cls, power:int) -> None: + if power < 0: + raise InvalidPowerValueError("Power level must be a positive integer") \ No newline at end of file From fb55b393c25b2e41aa25bec6955f6e114848732b Mon Sep 17 00:00:00 2001 From: asquator Date: Sun, 22 Dec 2024 12:51:01 +0200 Subject: [PATCH 03/13] Extracted shared attributes into an abstract class Bender --- tlab/air_bender.py | 42 +++----------------------------- tlab/bender.py | 58 ++++++++++++++++++++++++++++++++++++++++++++ tlab/earth_bender.py | 38 ++++++----------------------- tlab/fire_bender.py | 38 +++++------------------------ tlab/water_bender.py | 30 +++-------------------- 5 files changed, 77 insertions(+), 129 deletions(-) create mode 100644 tlab/bender.py diff --git a/tlab/air_bender.py b/tlab/air_bender.py index 72bed7c..54e64fc 100644 --- a/tlab/air_bender.py +++ b/tlab/air_bender.py @@ -1,47 +1,16 @@ from typing import Self - +from .bender import Bender from tlab.exceptions import InvalidPowerValueError, InvalidPowerTypeError -class AirBender: +class AirBender(Bender): # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( self: Self, name: str, power: int, ) -> None: - - self._validate_power(power) - - self._name = name - self._power = power - - @property - def name( - self: Self, - ) -> str: - return self._name - - @name.setter - def name( - self: Self, - name: str, - ) -> None: - self._name = name - - @property - def power( - self: Self, - ) -> int: - return self._power - - @power.setter - def power( - self: Self, - power: int, - ) -> None: - self._validate_power(power) - self._power = power + super().__init__(name, power) @property def skill( @@ -53,8 +22,3 @@ def bend( self: Self, ) -> None: raise NotImplementedError("You Should Implement this method") - - @classmethod - def _validate_power(cls, power:int) -> None: - if power < 0: - raise InvalidPowerValueError("Power level must be a positive integer") \ No newline at end of file diff --git a/tlab/bender.py b/tlab/bender.py new file mode 100644 index 0000000..96e10e1 --- /dev/null +++ b/tlab/bender.py @@ -0,0 +1,58 @@ +from typing import Self +from abc import ABC, abstractmethod +from tlab.exceptions import InvalidPowerValueError, InvalidPowerTypeError + +class Bender(ABC): + def __init__( + self: Self, + name: str, + power: int, + ) -> None: + self._validate_power(power) + + self._name = name + self._power = power + + @property + def name( + self: Self, + ) -> str: + return self._name + + @name.setter + def name( + self: Self, + name: str, + ) -> None: + self._name = name + + @property + def power( + self: Self, + ) -> int: + return self._power + + @power.setter + def power( + self: Self, + power: int, + ) -> None: + self._validate_power(power) + self._power = power + + @classmethod + def _validate_power(cls, power:int) -> None: + if power < 0: + raise InvalidPowerValueError("Power level must be a positive integer") + + @abstractmethod + def skill( + self: Self, + ) -> str: + pass + + @abstractmethod + def bend( + self: Self, + ) -> None: + pass \ No newline at end of file diff --git a/tlab/earth_bender.py b/tlab/earth_bender.py index b7dad70..1153cb2 100644 --- a/tlab/earth_bender.py +++ b/tlab/earth_bender.py @@ -1,38 +1,14 @@ from typing import Self -class EarthBender: +from .bender import Bender + +class EarthBender(Bender): # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( - self: Self, - name: str, - power: int, - ) -> None: - raise NotImplementedError("You Should Implement this method") - - @property - def name( - self: Self, - ) -> str: - raise NotImplementedError("You Should Implement this method") - - @name.setter - def name( - self: Self, - name: str, - ) -> None: - raise NotImplementedError("You Should Implement this method") - - @property - def power( - self: Self, - ) -> int: - raise NotImplementedError("You Should Implement this method") - - @power.setter - def power( - self: Self, - power: int, + self: Self, + name: str, + power: int, ) -> None: - raise NotImplementedError("You Should Implement this method") + super().__init__(name, power) @property def skill( diff --git a/tlab/fire_bender.py b/tlab/fire_bender.py index 8134a35..dbe6908 100644 --- a/tlab/fire_bender.py +++ b/tlab/fire_bender.py @@ -1,42 +1,16 @@ import random +from .bender import Bender from typing import Self -class FireBender: +class FireBender(Bender): # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( - self: Self, - name: str, - power: int, - random_generator: random.Random = random, - ) -> None: - raise NotImplementedError("You Should Implement this method") - - @property - def name( - self: Self, - ) -> str: - raise NotImplementedError("You Should Implement this method") - - @name.setter - def name( - self: Self, - name: str, + self: Self, + name: str, + power: int, ) -> None: - raise NotImplementedError("You Should Implement this method") - - @property - def power( - self: Self, - ) -> int: - raise NotImplementedError("You Should Implement this method") - - @power.setter - def power( - self: Self, - power: int, - ) -> None: - raise NotImplementedError("You Should Implement this method") + super().__init__(name, power) @property def skill( diff --git a/tlab/water_bender.py b/tlab/water_bender.py index 5080719..c0d9ab4 100644 --- a/tlab/water_bender.py +++ b/tlab/water_bender.py @@ -1,40 +1,16 @@ from typing import Self +from .bender import Bender from webbrowser import BaseBrowser -class WaterBender: +class WaterBender(Bender): # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( self: Self, name: str, power: int, - browser: BaseBrowser | None = None, ) -> None: - raise NotImplementedError("You Should Implement this method") - - @property - def name(self: Self) -> str: - raise NotImplementedError("You Should Implement this method") - - @name.setter - def name( - self: Self, - name: str, - ) -> None: - raise NotImplementedError("You Should Implement this method") - - @property - def power( - self: Self, - ) -> int: - raise NotImplementedError("You Should Implement this method") - - @power.setter - def power( - self: Self, - power: int, - ) -> None: - raise NotImplementedError("You Should Implement this method") + super().__init__(name, power) @property def skill( From 21dd7f9a8aea22dfa2e2c4327967e34342493438 Mon Sep 17 00:00:00 2001 From: asquator Date: Sun, 22 Dec 2024 13:07:32 +0200 Subject: [PATCH 04/13] Fixed power validation to use strict type checking --- tlab/bender.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tlab/bender.py b/tlab/bender.py index 96e10e1..f3d7cee 100644 --- a/tlab/bender.py +++ b/tlab/bender.py @@ -41,7 +41,10 @@ def power( self._power = power @classmethod - def _validate_power(cls, power:int) -> None: + def _validate_power(cls, power: int) -> None: + if type(power) is not int: + raise InvalidPowerTypeError("Power level must be a positive integer") + if power < 0: raise InvalidPowerValueError("Power level must be a positive integer") From bde53371e0724aac82c50d6a0623cae0dfb81156 Mon Sep 17 00:00:00 2001 From: asquator Date: Sun, 22 Dec 2024 13:28:49 +0200 Subject: [PATCH 05/13] Extracted skill as a special shared attribute --- tlab/air_bender.py | 13 +++++++------ tlab/bender.py | 15 +++++++++------ tlab/earth_bender.py | 14 +++++++------- tlab/fire_bender.py | 14 ++++++-------- tlab/water_bender.py | 20 +++++++++----------- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/tlab/air_bender.py b/tlab/air_bender.py index 54e64fc..b2cb146 100644 --- a/tlab/air_bender.py +++ b/tlab/air_bender.py @@ -4,19 +4,20 @@ class AirBender(Bender): + + base_skill = "Airbending" + # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( self: Self, name: str, power: int, + *, + skill=base_skill, ) -> None: - super().__init__(name, power) + super().__init__(name, power, skill=skill) + - @property - def skill( - self: Self, - ) -> str: - raise NotImplementedError("You Should Implement this method") def bend( self: Self, diff --git a/tlab/bender.py b/tlab/bender.py index f3d7cee..a34c15e 100644 --- a/tlab/bender.py +++ b/tlab/bender.py @@ -7,11 +7,14 @@ def __init__( self: Self, name: str, power: int, + *, + skill: str ) -> None: self._validate_power(power) self._name = name self._power = power + self._skill = skill @property def name( @@ -40,6 +43,12 @@ def power( self._validate_power(power) self._power = power + @property + def skill( + self: Self, + ) -> str: + return self._skill + @classmethod def _validate_power(cls, power: int) -> None: if type(power) is not int: @@ -48,12 +57,6 @@ def _validate_power(cls, power: int) -> None: if power < 0: raise InvalidPowerValueError("Power level must be a positive integer") - @abstractmethod - def skill( - self: Self, - ) -> str: - pass - @abstractmethod def bend( self: Self, diff --git a/tlab/earth_bender.py b/tlab/earth_bender.py index 1153cb2..8ac63a4 100644 --- a/tlab/earth_bender.py +++ b/tlab/earth_bender.py @@ -1,22 +1,22 @@ from typing import Self from .bender import Bender + class EarthBender(Bender): + base_skill = "Earthbending" + # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( self: Self, name: str, power: int, + *, + skill=base_skill ) -> None: - super().__init__(name, power) + super().__init__(name, power, skill=skill) - @property - def skill( - self: Self, - ) -> str: - raise NotImplementedError("You Should Implement this method") def bend( - self: Self, + self: Self, ) -> None: raise NotImplementedError("You Should Implement this method") diff --git a/tlab/fire_bender.py b/tlab/fire_bender.py index dbe6908..3ca849f 100644 --- a/tlab/fire_bender.py +++ b/tlab/fire_bender.py @@ -4,21 +4,19 @@ class FireBender(Bender): + base_skill = "Firebending" + # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( self: Self, name: str, power: int, + *, + skill=base_skill ) -> None: - super().__init__(name, power) - - @property - def skill( - self: Self, - ) -> str: - raise NotImplementedError("You Should Implement this method") + super().__init__(name, power, skill=skill) def bend( - self: Self, + self: Self, ) -> None: raise NotImplementedError("You Should Implement this method") diff --git a/tlab/water_bender.py b/tlab/water_bender.py index c0d9ab4..504d653 100644 --- a/tlab/water_bender.py +++ b/tlab/water_bender.py @@ -4,21 +4,19 @@ class WaterBender(Bender): + base_skill = "Waterbending" + # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( - self: Self, - name: str, - power: int, + self: Self, + name: str, + power: int, + *, + skill=base_skill ) -> None: - super().__init__(name, power) - - @property - def skill( - self: Self, - ) -> str: - raise NotImplementedError("You Should Implement this method") + super().__init__(name, power, skill=skill) def bend( - self: Self, + self: Self, ) -> None: raise NotImplementedError("You Should Implement this method") From 49b8eae5fe04c932d3781c5069067dbc8d1093f3 Mon Sep 17 00:00:00 2001 From: asquator Date: Sun, 22 Dec 2024 15:16:49 +0200 Subject: [PATCH 06/13] Implemented bend() in EarthBender --- tlab/bender.py | 1 + tlab/earth_bender.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tlab/bender.py b/tlab/bender.py index a34c15e..dbabe58 100644 --- a/tlab/bender.py +++ b/tlab/bender.py @@ -57,6 +57,7 @@ def _validate_power(cls, power: int) -> None: if power < 0: raise InvalidPowerValueError("Power level must be a positive integer") + @abstractmethod def bend( self: Self, diff --git a/tlab/earth_bender.py b/tlab/earth_bender.py index 8ac63a4..318bacf 100644 --- a/tlab/earth_bender.py +++ b/tlab/earth_bender.py @@ -1,9 +1,19 @@ +import os +import re +import sys from typing import Self from .bender import Bender class EarthBender(Bender): base_skill = "Earthbending" + attack_var_name = "EARTH_ATTACK" + expected_attack = r"rock ball" + expected_attack_regex = re.compile( + expected_attack, + re.IGNORECASE, + ) + unexpected_attack_var_value = "No Rock Ball :(" # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( @@ -19,4 +29,13 @@ def __init__( def bend( self: Self, ) -> None: - raise NotImplementedError("You Should Implement this method") + attack = os.environ[EarthBender.attack_var_name] + + if re.fullmatch(self.expected_attack_regex, attack): + self.emit_expected_attack() + return + + os.environ[EarthBender.attack_var_name] = EarthBender.unexpected_attack_var_value + + def emit_expected_attack(self, file=sys.stdout) -> None: + print(f"{self.expected_attack} with power: {self.power:^2}.", end='', file=sys.stdout) \ No newline at end of file From e97fc54481191753d6f12aced4c6a58d960c8f43 Mon Sep 17 00:00:00 2001 From: asquator Date: Sun, 22 Dec 2024 15:25:49 +0200 Subject: [PATCH 07/13] Implemented bend() in AirBender --- tlab/air_bender.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tlab/air_bender.py b/tlab/air_bender.py index b2cb146..680eaef 100644 --- a/tlab/air_bender.py +++ b/tlab/air_bender.py @@ -1,8 +1,8 @@ from typing import Self from .bender import Bender +import logging from tlab.exceptions import InvalidPowerValueError, InvalidPowerTypeError - class AirBender(Bender): base_skill = "Airbending" @@ -18,8 +18,7 @@ def __init__( super().__init__(name, power, skill=skill) - def bend( self: Self, ) -> None: - raise NotImplementedError("You Should Implement this method") + logging.info(f"{self.name} is using his {self.skill.lower()} skill!") \ No newline at end of file From ed9a329b8c344e29b7f25aada4429e482d789a2f Mon Sep 17 00:00:00 2001 From: asquator Date: Sun, 22 Dec 2024 16:28:28 +0200 Subject: [PATCH 08/13] Implemented bend() in FireBender --- tlab/fire_bender.py | 23 ++++++++++++++++++++--- tlab/handler_manager.py | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tlab/handler_manager.py diff --git a/tlab/fire_bender.py b/tlab/fire_bender.py index 3ca849f..fa656b3 100644 --- a/tlab/fire_bender.py +++ b/tlab/fire_bender.py @@ -1,22 +1,39 @@ import random from .bender import Bender from typing import Self +from .handler_manager import HandlerManager - -class FireBender(Bender): +class FireBender(Bender, HandlerManager): base_skill = "Firebending" + attacked_name = "dead" + + # special days + eclipse_day = 0 + sozins_comet_day = 6 # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( self: Self, name: str, power: int, + generator: random.Random = random.Random(), *, skill=base_skill ) -> None: super().__init__(name, power, skill=skill) + self.generator = generator def bend( self: Self, ) -> None: - raise NotImplementedError("You Should Implement this method") + value = self.generator.randint(0, 6) + self.handle(key=value) + + @HandlerManager.register_handler(key=eclipse_day) + def _eclipse_attack(self): + self.name = self.attacked_name + + @HandlerManager.register_handler(key=sozins_comet_day) + def _sozins_comet_attack(self): + raise SystemExit(self.sozins_comet_day) + diff --git a/tlab/handler_manager.py b/tlab/handler_manager.py new file mode 100644 index 0000000..9bd5987 --- /dev/null +++ b/tlab/handler_manager.py @@ -0,0 +1,23 @@ +class HandlerManager: + """ + Generic mixin class that allows registering methods as handlers referenced by a key + """ + + handlers = dict() # dictionary of handlers + + @classmethod + def register_handler(cls, key): + + def decorator(func): + cls.handlers[key] = func + return func + + return decorator + + def handle(self, key, *args): + handler = self.handlers.get(key) + + if handler is not None: + return self.handlers[key](self, *args) + + return None From 3ac91179081dff665c8909b014f0be828e139cb0 Mon Sep 17 00:00:00 2001 From: asquator Date: Mon, 23 Dec 2024 09:46:36 +0200 Subject: [PATCH 09/13] Implemented bend() in WaterBender --- tlab/water_bender.py | 56 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/tlab/water_bender.py b/tlab/water_bender.py index 504d653..abb5336 100644 --- a/tlab/water_bender.py +++ b/tlab/water_bender.py @@ -1,22 +1,72 @@ +import os +from collections.abc import Iterable from typing import Self -from .bender import Bender from webbrowser import BaseBrowser +from .bender import Bender + class WaterBender(Bender): base_skill = "Waterbending" + # links to open on matching patterns + default_moon_links = { + "FULL": "https://www.wikiwand.com/en/6", + None: "https://youtu.be/gk-aCL6eyGc?si=XX45XZzc3a8uCN0o&t={}", + } + + # links to open when on no matching patterns + default_fallover_links = { + "https://youtu.be/weZKm1kTrpc?si=_Unblsn5tPvzwfs7", + } + + # special days # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( self: Self, name: str, power: int, + browser: BaseBrowser = None, + moon_links=None, + fallover_links=None, *, - skill=base_skill + skill=base_skill, ) -> None: super().__init__(name, power, skill=skill) + self.browser = browser + + if moon_links is None: + moon_links = WaterBender.default_moon_links + + if fallover_links is None: + fallover_links = WaterBender.default_fallover_links + + self._moon_links = moon_links + self._fallover_links = fallover_links def bend( self: Self, ) -> None: - raise NotImplementedError("You Should Implement this method") + self._update_power() + if self.browser is None: + return + + links = self._links_to_open() + self._open_links(links) + + def _links_to_open(self) -> Iterable[str]: + moon_status = os.getenv("MOON") + link = self._moon_links.get(moon_status) + + if link is not None: + return {link.format(self.power)} + + return self._fallover_links + + def _open_links(self, links: Iterable[str]) -> None: + for link in links: + self.browser.open_new(link) + + def _update_power(self) -> None: + if self.browser is None and self.power > 0: + self.power -= 1 From 520743294cccb1af83f2524065f6e2816133b037 Mon Sep 17 00:00:00 2001 From: asquator Date: Mon, 23 Dec 2024 09:55:36 +0200 Subject: [PATCH 10/13] Linting run --- tlab/air_bender.py | 9 ++++----- tlab/bender.py | 35 ++++++++++++++++++----------------- tlab/earth_bender.py | 24 +++++++++++++++--------- tlab/fire_bender.py | 19 ++++++++++--------- tlab/handler_manager.py | 1 - tlab/water_bender.py | 18 +++++++++--------- 6 files changed, 56 insertions(+), 50 deletions(-) diff --git a/tlab/air_bender.py b/tlab/air_bender.py index 680eaef..9897c68 100644 --- a/tlab/air_bender.py +++ b/tlab/air_bender.py @@ -1,10 +1,10 @@ +import logging from typing import Self + from .bender import Bender -import logging -from tlab.exceptions import InvalidPowerValueError, InvalidPowerTypeError -class AirBender(Bender): +class AirBender(Bender): base_skill = "Airbending" # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations @@ -17,8 +17,7 @@ def __init__( ) -> None: super().__init__(name, power, skill=skill) - def bend( self: Self, ) -> None: - logging.info(f"{self.name} is using his {self.skill.lower()} skill!") \ No newline at end of file + logging.info(f"{self.name} is using his {self.skill.lower()} skill!") diff --git a/tlab/bender.py b/tlab/bender.py index dbabe58..22656d8 100644 --- a/tlab/bender.py +++ b/tlab/bender.py @@ -1,14 +1,16 @@ -from typing import Self from abc import ABC, abstractmethod -from tlab.exceptions import InvalidPowerValueError, InvalidPowerTypeError +from typing import Self + +from tlab.exceptions import InvalidPowerTypeError, InvalidPowerValueError + class Bender(ABC): def __init__( - self: Self, - name: str, - power: int, - *, - skill: str + self: Self, + name: str, + power: int, + *, + skill: str, ) -> None: self._validate_power(power) @@ -18,34 +20,34 @@ def __init__( @property def name( - self: Self, + self: Self, ) -> str: return self._name @name.setter def name( - self: Self, - name: str, + self: Self, + name: str, ) -> None: self._name = name @property def power( - self: Self, + self: Self, ) -> int: return self._power @power.setter def power( - self: Self, - power: int, + self: Self, + power: int, ) -> None: self._validate_power(power) self._power = power @property def skill( - self: Self, + self: Self, ) -> str: return self._skill @@ -57,9 +59,8 @@ def _validate_power(cls, power: int) -> None: if power < 0: raise InvalidPowerValueError("Power level must be a positive integer") - @abstractmethod def bend( - self: Self, + self: Self, ) -> None: - pass \ No newline at end of file + pass diff --git a/tlab/earth_bender.py b/tlab/earth_bender.py index 318bacf..4c17fd1 100644 --- a/tlab/earth_bender.py +++ b/tlab/earth_bender.py @@ -2,6 +2,7 @@ import re import sys from typing import Self + from .bender import Bender @@ -17,17 +18,16 @@ class EarthBender(Bender): # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( - self: Self, - name: str, - power: int, - *, - skill=base_skill + self: Self, + name: str, + power: int, + *, + skill=base_skill, ) -> None: super().__init__(name, power, skill=skill) - def bend( - self: Self, + self: Self, ) -> None: attack = os.environ[EarthBender.attack_var_name] @@ -35,7 +35,13 @@ def bend( self.emit_expected_attack() return - os.environ[EarthBender.attack_var_name] = EarthBender.unexpected_attack_var_value + os.environ[EarthBender.attack_var_name] = ( + EarthBender.unexpected_attack_var_value + ) def emit_expected_attack(self, file=sys.stdout) -> None: - print(f"{self.expected_attack} with power: {self.power:^2}.", end='', file=sys.stdout) \ No newline at end of file + print( + f"{self.expected_attack} with power: {self.power:^2}.", + end="", + file=sys.stdout, + ) diff --git a/tlab/fire_bender.py b/tlab/fire_bender.py index fa656b3..7bd4177 100644 --- a/tlab/fire_bender.py +++ b/tlab/fire_bender.py @@ -1,8 +1,10 @@ import random -from .bender import Bender from typing import Self + +from .bender import Bender from .handler_manager import HandlerManager + class FireBender(Bender, HandlerManager): base_skill = "Firebending" attacked_name = "dead" @@ -13,18 +15,18 @@ class FireBender(Bender, HandlerManager): # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( - self: Self, - name: str, - power: int, - generator: random.Random = random.Random(), - *, - skill=base_skill + self: Self, + name: str, + power: int, + generator: random.Random = random.Random(), + *, + skill=base_skill, ) -> None: super().__init__(name, power, skill=skill) self.generator = generator def bend( - self: Self, + self: Self, ) -> None: value = self.generator.randint(0, 6) self.handle(key=value) @@ -36,4 +38,3 @@ def _eclipse_attack(self): @HandlerManager.register_handler(key=sozins_comet_day) def _sozins_comet_attack(self): raise SystemExit(self.sozins_comet_day) - diff --git a/tlab/handler_manager.py b/tlab/handler_manager.py index 9bd5987..9191c59 100644 --- a/tlab/handler_manager.py +++ b/tlab/handler_manager.py @@ -7,7 +7,6 @@ class HandlerManager: @classmethod def register_handler(cls, key): - def decorator(func): cls.handlers[key] = func return func diff --git a/tlab/water_bender.py b/tlab/water_bender.py index abb5336..4a13f35 100644 --- a/tlab/water_bender.py +++ b/tlab/water_bender.py @@ -23,14 +23,14 @@ class WaterBender(Bender): # special days # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( - self: Self, - name: str, - power: int, - browser: BaseBrowser = None, - moon_links=None, - fallover_links=None, - *, - skill=base_skill, + self: Self, + name: str, + power: int, + browser: BaseBrowser | None = None, + moon_links: dict | None = None, + fallover_links: set | None = None, + *, + skill: str = base_skill, ) -> None: super().__init__(name, power, skill=skill) self.browser = browser @@ -45,7 +45,7 @@ def __init__( self._fallover_links = fallover_links def bend( - self: Self, + self: Self, ) -> None: self._update_power() if self.browser is None: From 016b2c27416e7163b24f300ad39c0484199b7c72 Mon Sep 17 00:00:00 2001 From: asquator Date: Mon, 23 Dec 2024 10:33:38 +0200 Subject: [PATCH 11/13] Extracted LinkManager from WaterBender --- tlab/link_manager.py | 41 +++++++++++++++++++++++++++++++++++++++++ tlab/water_bender.py | 38 +++++++++----------------------------- 2 files changed, 50 insertions(+), 29 deletions(-) create mode 100644 tlab/link_manager.py diff --git a/tlab/link_manager.py b/tlab/link_manager.py new file mode 100644 index 0000000..7a62673 --- /dev/null +++ b/tlab/link_manager.py @@ -0,0 +1,41 @@ +from abc import ABC, abstractmethod +from collections.abc import Iterable + + +class LinkManager(ABC): + @abstractmethod + def link(self, str) -> str: + pass + + def fallover_links(self) -> Iterable[str]: + pass + + +class DefaultLinkManager(LinkManager): + # links to open on matching patterns + DEFAULT_LINKS = { + "FULL": "https://www.wikiwand.com/en/6", + None: "https://youtu.be/gk-aCL6eyGc?si=XX45XZzc3a8uCN0o&t={}", + } + + # links to open when on no matching patterns + DEFAULT_FALLOVER_LINKS = { + "https://youtu.be/weZKm1kTrpc?si=_Unblsn5tPvzwfs7", + } + + def __init__(self, + links=None, + fallover_links=None): + if links is None: + links = self.DEFAULT_LINKS + if fallover_links is None: + fallover_links = self.DEFAULT_FALLOVER_LINKS + + self._links = links + self._fallover_links = fallover_links + + def link(self, str) -> str: + return self._links.get(str) + + def fallover_links(self) -> Iterable[str]: + return self._fallover_links diff --git a/tlab/water_bender.py b/tlab/water_bender.py index 4a13f35..22bc4a8 100644 --- a/tlab/water_bender.py +++ b/tlab/water_bender.py @@ -4,51 +4,31 @@ from webbrowser import BaseBrowser from .bender import Bender +from .link_manager import DefaultLinkManager, LinkManager class WaterBender(Bender): base_skill = "Waterbending" - # links to open on matching patterns - default_moon_links = { - "FULL": "https://www.wikiwand.com/en/6", - None: "https://youtu.be/gk-aCL6eyGc?si=XX45XZzc3a8uCN0o&t={}", - } - - # links to open when on no matching patterns - default_fallover_links = { - "https://youtu.be/weZKm1kTrpc?si=_Unblsn5tPvzwfs7", - } - - # special days # Return type is None due to: https://peps.python.org/pep-0484/#the-meaning-of-annotations def __init__( self: Self, name: str, power: int, browser: BaseBrowser | None = None, - moon_links: dict | None = None, - fallover_links: set | None = None, + link_manager: LinkManager = DefaultLinkManager(), *, skill: str = base_skill, ) -> None: super().__init__(name, power, skill=skill) - self.browser = browser - - if moon_links is None: - moon_links = WaterBender.default_moon_links - - if fallover_links is None: - fallover_links = WaterBender.default_fallover_links - - self._moon_links = moon_links - self._fallover_links = fallover_links + self._browser = browser + self._link_manager = link_manager def bend( self: Self, ) -> None: self._update_power() - if self.browser is None: + if self._browser is None: return links = self._links_to_open() @@ -56,17 +36,17 @@ def bend( def _links_to_open(self) -> Iterable[str]: moon_status = os.getenv("MOON") - link = self._moon_links.get(moon_status) + link = self._link_manager.link(moon_status) if link is not None: return {link.format(self.power)} - return self._fallover_links + return self._link_manager.fallover_links() def _open_links(self, links: Iterable[str]) -> None: for link in links: - self.browser.open_new(link) + self._browser.open_new(link) def _update_power(self) -> None: - if self.browser is None and self.power > 0: + if self._browser is None and self.power > 0: self.power -= 1 From d0a77145fff5c2432c940fbfb50b38daa72266fa Mon Sep 17 00:00:00 2001 From: asquator Date: Mon, 23 Dec 2024 11:13:18 +0200 Subject: [PATCH 12/13] Linting and formatting --- tlab/air_bender.py | 2 +- tlab/earth_bender.py | 5 +++-- tlab/fire_bender.py | 13 ++++++++----- tlab/handler_manager.py | 10 +++++++--- tlab/link_manager.py | 24 ++++++++++++++---------- tlab/water_bender.py | 5 ++++- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/tlab/air_bender.py b/tlab/air_bender.py index 9897c68..156636f 100644 --- a/tlab/air_bender.py +++ b/tlab/air_bender.py @@ -13,7 +13,7 @@ def __init__( name: str, power: int, *, - skill=base_skill, + skill: str = base_skill, ) -> None: super().__init__(name, power, skill=skill) diff --git a/tlab/earth_bender.py b/tlab/earth_bender.py index 4c17fd1..7a7b1fd 100644 --- a/tlab/earth_bender.py +++ b/tlab/earth_bender.py @@ -22,7 +22,7 @@ def __init__( name: str, power: int, *, - skill=base_skill, + skill: str = base_skill, ) -> None: super().__init__(name, power, skill=skill) @@ -39,9 +39,10 @@ def bend( EarthBender.unexpected_attack_var_value ) - def emit_expected_attack(self, file=sys.stdout) -> None: + def emit_expected_attack(self) -> None: print( f"{self.expected_attack} with power: {self.power:^2}.", end="", file=sys.stdout, + flush=True, ) diff --git a/tlab/fire_bender.py b/tlab/fire_bender.py index 7bd4177..3116a90 100644 --- a/tlab/fire_bender.py +++ b/tlab/fire_bender.py @@ -1,5 +1,5 @@ import random -from typing import Self +from typing import Never, Self from .bender import Bender from .handler_manager import HandlerManager @@ -18,11 +18,14 @@ def __init__( self: Self, name: str, power: int, - generator: random.Random = random.Random(), + generator: random.Random | None = None, *, - skill=base_skill, + skill: str = base_skill, ) -> None: super().__init__(name, power, skill=skill) + if generator is None: + generator = random.Random() + self.generator = generator def bend( @@ -32,9 +35,9 @@ def bend( self.handle(key=value) @HandlerManager.register_handler(key=eclipse_day) - def _eclipse_attack(self): + def _eclipse_attack(self) -> None: self.name = self.attacked_name @HandlerManager.register_handler(key=sozins_comet_day) - def _sozins_comet_attack(self): + def _sozins_comet_attack(self) -> Never: raise SystemExit(self.sozins_comet_day) diff --git a/tlab/handler_manager.py b/tlab/handler_manager.py index 9191c59..dda34cf 100644 --- a/tlab/handler_manager.py +++ b/tlab/handler_manager.py @@ -1,19 +1,23 @@ +from collections.abc import Callable +from typing import Any, ClassVar + + class HandlerManager: """ Generic mixin class that allows registering methods as handlers referenced by a key """ - handlers = dict() # dictionary of handlers + handlers: ClassVar[dict[Any, Callable]] = {} # dictionary of handlers @classmethod - def register_handler(cls, key): + def register_handler(cls, key) -> Callable: def decorator(func): cls.handlers[key] = func return func return decorator - def handle(self, key, *args): + def handle(self, key, *args: list) -> Any: handler = self.handlers.get(key) if handler is not None: diff --git a/tlab/link_manager.py b/tlab/link_manager.py index 7a62673..2521a47 100644 --- a/tlab/link_manager.py +++ b/tlab/link_manager.py @@ -1,41 +1,45 @@ from abc import ABC, abstractmethod from collections.abc import Iterable +from typing import Any, ClassVar class LinkManager(ABC): @abstractmethod - def link(self, str) -> str: + def link(self, link: str) -> str: pass + @abstractmethod def fallover_links(self) -> Iterable[str]: pass class DefaultLinkManager(LinkManager): # links to open on matching patterns - DEFAULT_LINKS = { + default_links: ClassVar[dict[Any, str]] = { "FULL": "https://www.wikiwand.com/en/6", None: "https://youtu.be/gk-aCL6eyGc?si=XX45XZzc3a8uCN0o&t={}", } # links to open when on no matching patterns - DEFAULT_FALLOVER_LINKS = { + default_failover_links: ClassVar[set[str]] = { "https://youtu.be/weZKm1kTrpc?si=_Unblsn5tPvzwfs7", } - def __init__(self, - links=None, - fallover_links=None): + def __init__( + self, + links: dict[Any, str] | None = None, + fallover_links: set[str] | None = None, + ) -> None: if links is None: - links = self.DEFAULT_LINKS + links = self.default_links if fallover_links is None: - fallover_links = self.DEFAULT_FALLOVER_LINKS + fallover_links = self.default_failover_links self._links = links self._fallover_links = fallover_links - def link(self, str) -> str: - return self._links.get(str) + def link(self, link: str) -> str: + return self._links.get(link) def fallover_links(self) -> Iterable[str]: return self._fallover_links diff --git a/tlab/water_bender.py b/tlab/water_bender.py index 22bc4a8..c1f25c4 100644 --- a/tlab/water_bender.py +++ b/tlab/water_bender.py @@ -16,7 +16,7 @@ def __init__( name: str, power: int, browser: BaseBrowser | None = None, - link_manager: LinkManager = DefaultLinkManager(), + link_manager: LinkManager = None, *, skill: str = base_skill, ) -> None: @@ -24,6 +24,9 @@ def __init__( self._browser = browser self._link_manager = link_manager + if link_manager is None: + self._link_manager = DefaultLinkManager() + def bend( self: Self, ) -> None: From bbc11693cace6c73b5966158fefc2965fcb29392 Mon Sep 17 00:00:00 2001 From: asquator Date: Mon, 23 Dec 2024 12:33:21 +0200 Subject: [PATCH 13/13] Added generic hints to HandlerManager --- tlab/handler_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tlab/handler_manager.py b/tlab/handler_manager.py index dda34cf..cd2c304 100644 --- a/tlab/handler_manager.py +++ b/tlab/handler_manager.py @@ -2,7 +2,7 @@ from typing import Any, ClassVar -class HandlerManager: +class HandlerManager[K, V]: """ Generic mixin class that allows registering methods as handlers referenced by a key """ @@ -10,14 +10,14 @@ class HandlerManager: handlers: ClassVar[dict[Any, Callable]] = {} # dictionary of handlers @classmethod - def register_handler(cls, key) -> Callable: - def decorator(func): + def register_handler(cls, key: K) -> Callable: + def decorator(func: Callable) -> Callable: cls.handlers[key] = func return func return decorator - def handle(self, key, *args: list) -> Any: + def handle(self, key: K, *args: list) -> V: handler = self.handlers.get(key) if handler is not None: