diff --git a/README.md b/README.md index 8ca9e78..108128f 100644 --- a/README.md +++ b/README.md @@ -71,4 +71,32 @@ Pour exécuter ce projet, vous aurez besoin de Python 3.7 ou supérieur. ```bash python3 app.py -``` \ No newline at end of file +``` + +## Personnalisation du CharacterBuilder + +Le `CharacterBuilder` peut être personnalisé pour utiliser différentes implémentations de sauvegarde et de chargement selon vos besoins. +Cette personnalisation se fait simplement en définissant une variable d'environnement avant de lancer votre script Python. + +### Variable d'environnement : `ELIRON_BUILDER` + +- **Nom** : `ELIRON_BUILDER` +- **Valeur possible** : + - `"pydantic"` : Utilise des implementations basées sur `pydantic`. + - Toute autre valeur ou absence de variable : Utilise les fonctions par défaut. + +> ⚠️ **Important** : Pour utiliser le builder pydantic, vous devez avoir installé le package `pydantic` au préalable : +> ```bash +> pip install pydantic +> ``` + +### Exemple d'utilisation sous Linux / macOS + +```bash +# Pour utiliser l'implémentation pydantic +export ELIRON_BUILDER=pydantic +python app.py + +# Pour utiliser l'implémentation par defaut +unset ELIRON_BUILDER +python app.py diff --git a/character_builder.py b/character_builder.py index f8e8dbf..fa06a6f 100644 --- a/character_builder.py +++ b/character_builder.py @@ -1,11 +1,18 @@ -from base_mob import BaseMob +import importlib.util import json import os +import sys +from typing import Callable + +from base_mob import BaseMob class CharacterBuilder: - @staticmethod - def build_character(): + save_character_impl: Callable[[BaseMob], str] + load_character_impl: Callable[[str], None] + + @classmethod + def build_character(cls): """Demande à l'utilisateur de saisir les attributs du personnage.""" print("Création d'un nouveau personnage :") name = input("Nom du personnage : ") @@ -16,51 +23,83 @@ class CharacterBuilder: mob_type = 0 return BaseMob(name, max_pv, strength, protection, speed, mob_type) - @staticmethod - def save_character(character): + @classmethod + def save_character(cls, character: BaseMob) -> None: """Enregistre un personnage dans un fichier JSON.""" - character_data = { - "name": character.name, - "max_pv": character.max_pv, - "current_pv": character.current_pv, - "strength": character.strength, - "protection": character.protection, - "speed": character.speed, - "mob_type": character.mob_type - } - filename = character.name + ".json" - with open(filename, "w",encoding='UTF-8') as file: - json.dump(character_data, file, indent=4) + filename = cls.save_character_impl(character) print(f"Personnage enregistré dans {filename}.") - @staticmethod - def load_character(name): + @classmethod + def load_character(cls, name: str) -> BaseMob | None: """Charge un personnage à partir d'un fichier JSON.""" - filename = name + ".json" - if not os.path.exists(filename): - print(f"Erreur : Le fichier {filename} n'existe pas.") - return None - try: - with open(filename, "r",encoding='UTF-8') as file: - character_data = json.load(file) - - # Vérification des clés essentielles dans le JSON pour éviter les erreurs - required_keys = ["name", "max_pv", "current_pv", - "strength", "protection", "speed", "mob_type"] - if not all(key in character_data for key in required_keys): - print(f"Erreur : Le fichier {filename} ne contient pas toutes les clés nécessaires.") - return None - - # Construction du personnage - return BaseMob( - name=character_data["name"], - max_pv=character_data["max_pv"], - strength=character_data["strength"], - protection=character_data["protection"], - speed=character_data["speed"], - mob_type=character_data["mob_type"] - ) - except json.JSONDecodeError: - print(f"Erreur : Le fichier {filename} n'est pas un JSON valide.") + return cls.load_character_impl(name) + except Exception as e: return None + + +def save_character(character: BaseMob) -> str: + """Enregistre un personnage dans un fichier JSON.""" + character_data = { + "name": character.name, + "max_pv": character.max_pv, + "current_pv": character.current_pv, + "strength": character.strength, + "protection": character.protection, + "speed": character.speed, + "mob_type": character.mob_type + } + filename = character.name + ".json" + with open(filename, "w", encoding='UTF-8') as file: + json.dump(character_data, file, indent=4) + return filename + + +def load_character(name: str) -> BaseMob | None: + """Charge un personnage à partir d'un fichier JSON.""" + filename = name + ".json" + if not os.path.exists(filename): + print(f"Erreur : Le fichier {filename} n'existe pas.") + return None + + try: + with open(filename, "r", encoding='UTF-8') as file: + character_data = json.load(file) + + # Vérification des clés essentielles dans le JSON pour éviter les erreurs + required_keys = ["name", "max_pv", "current_pv", + "strength", "protection", "speed", "mob_type"] + if not all(key in character_data for key in required_keys): + print(f"Erreur : Le fichier {filename} ne contient pas toutes les clés nécessaires.") + return None + + # Construction du personnage + return BaseMob( + name=character_data["name"], + max_pv=character_data["max_pv"], + strength=character_data["strength"], + protection=character_data["protection"], + speed=character_data["speed"], + mob_type=character_data["mob_type"] + ) + except json.JSONDecodeError: + print(f"Erreur : Le fichier {filename} n'est pas un JSON valide.") + return None + + +pydantic = None +name = 'pydantic' +if name in sys.modules: + pydantic = sys.modules[name] +elif (spec := importlib.util.find_spec(name)) is not None: + pydantic = importlib.util.module_from_spec(spec) + sys.modules[name] = pydantic + spec.loader.exec_module(pydantic) + +if pydantic and os.environ.get('ELIRON_BUILDER') == 'pydantic': + pydantic_character_builder = importlib.import_module('pydantic_character_builder') + CharacterBuilder.save_character_impl = pydantic_character_builder.save_character + CharacterBuilder.load_character_impl = pydantic_character_builder.load_character +else: + CharacterBuilder.save_character_impl = save_character + CharacterBuilder.load_character_impl = load_character diff --git a/pydantic_character_builder.py b/pydantic_character_builder.py new file mode 100644 index 0000000..cd6ec29 --- /dev/null +++ b/pydantic_character_builder.py @@ -0,0 +1,46 @@ +import pathlib + +from pydantic import BaseModel + +from base_mob import BaseMob + + +class CharacterModel(BaseModel): + name: str + max_pv: int + current_pv: int + strength: int + protection: int + speed: int + mob_type: int + + +def save_character(character: BaseMob) -> str: + filename = f'{character.name}.json' + character_model = CharacterModel( + name=character.name, + max_pv=character.max_pv, + current_pv=character.current_pv, + strength=character.strength, + protection=character.protection, + speed=character.speed, + mob_type=character.mob_type + ) + pathlib.Path(filename).write_text( + data=character_model.model_dump_json(indent=4), + encoding='utf-8' + ) + return filename + + +def load_character(name: str) -> BaseMob | None: + json_data = pathlib.Path(f'{name}.json').read_text(encoding='utf-8') + character_model = CharacterModel.model_validate_json(json_data) + return BaseMob( + name=character_model.name, + max_pv=character_model.max_pv, + strength=character_model.strength, + protection=character_model.protection, + speed=character_model.speed, + mob_type=character_model.mob_type, + ) diff --git a/test_character_builder.py b/test_character_builder.py new file mode 100644 index 0000000..135f49f --- /dev/null +++ b/test_character_builder.py @@ -0,0 +1,121 @@ +import unittest +from unittest.mock import patch +import json +import pathlib + +from base_mob import BaseMob +from character_builder import CharacterBuilder + +ARTHUR_JSON = """{ + "name": "Arthur", + "max_pv": 100, + "current_pv": 100, + "strength": 20, + "protection": 10, + "speed": 5, + "mob_type": 0 +}""" + +ARTHUR_FILE = pathlib.Path('Arthur.json') + + +class TestCharacterBuilder(unittest.TestCase): + + @classmethod + def tearDownClass(cls): + ARTHUR_FILE.unlink(missing_ok=True) + + @patch("builtins.input") + def test_build_character(self, mock_input): + # Arrange + mock_input.side_effect = [ + "Arthur", # name + "100", # max_pv + "20", # strength + "10", # protection + "5" # speed + ] + + # Act + character = CharacterBuilder.build_character() + + # Assert + self.assertIsInstance(character, BaseMob) + self.assertEqual(character.name, "Arthur") + self.assertEqual(character.max_pv, 100) + self.assertEqual(character.strength, 20) + self.assertEqual(character.protection, 10) + self.assertEqual(character.speed, 5) + self.assertEqual(character.mob_type, 0) + + def test_save_character(self): + # Arrange + character = BaseMob( + name="Arthur", + max_pv=100, + strength=20, + protection=10, + speed=5, + mob_type=0, + ) + + # Act + CharacterBuilder.save_character(character) + + # Assert + self.assertTrue(ARTHUR_FILE.exists()) + self.assertEqual(ARTHUR_JSON, ARTHUR_FILE.read_text(encoding='utf-8')) + + def test_load_character_file_not_found(self): + # Act + result = CharacterBuilder.load_character("Lancelot") + + # Assert + self.assertIsNone(result) + + def test_load_character_success(self): + # Arrange + ARTHUR_FILE.write_text(ARTHUR_JSON, encoding='utf-8') + + # Act + character = CharacterBuilder.load_character("Arthur") + + # Assert + self.assertIsNotNone(character) + self.assertEqual(character.name, "Arthur") + self.assertEqual(character.max_pv, 100) + self.assertEqual(character.strength, 20) + self.assertEqual(character.protection, 10) + self.assertEqual(character.speed, 5) + self.assertEqual(character.mob_type, 0) + + def test_load_character_missing_keys(self): + arthur_dict: dict = json.loads(ARTHUR_JSON) + + for key in arthur_dict: + with self.subTest(f'test without key "{key}"'): + # Arrange + invalid_dict = arthur_dict.copy() + invalid_dict.pop(key) + ARTHUR_FILE.write_text(json.dumps( + invalid_dict), encoding='utf-8') + + # Act + result = CharacterBuilder.load_character("Arthur") + + # Assert + self.assertIsNone(result) + + def test_load_character_invalid_json(self): + # Arrange + ARTHUR_FILE.write_text(ARTHUR_JSON[1:], encoding='utf-8') + + # Act + result = CharacterBuilder.load_character("Arthur") + + # Assert + self.assertIsNone(result) + + +if __name__ == "__main__": + unittest.main()