From 722bd8a00ccaa6c249ee1a98034a5993b5fe657e Mon Sep 17 00:00:00 2001 From: toktoktheeo <34038708+toktoktheeo@users.noreply.github.com> Date: Sun, 20 Mar 2022 20:47:48 +0100 Subject: [PATCH 01/11] Updated fr po files 25% -> 78% completed --- evennia/locale/fr/LC_MESSAGES/django.po | 841 +++++++++++++----------- 1 file changed, 451 insertions(+), 390 deletions(-) diff --git a/evennia/locale/fr/LC_MESSAGES/django.po b/evennia/locale/fr/LC_MESSAGES/django.po index 04561c9835..67e43a8003 100644 --- a/evennia/locale/fr/LC_MESSAGES/django.po +++ b/evennia/locale/fr/LC_MESSAGES/django.po @@ -7,182 +7,216 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-05-29 16:24+0000\n" -"PO-Revision-Date: 2016-03-04 11:51-0500\n" -"Last-Translator: \n" +"POT-Creation-Date: 2022-03-20 14:48+0000\n" +"PO-Revision-Date: 2022-03-20 19:55+0100\n" +"Last-Translator: Christophe Petry \n" "Language-Team: \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Generator: Poedit 1.7.6\n" +"X-Generator: Poedit 3.0.1\n" -#: accounts/accounts.py:321 +#: accounts/accounts.py:322 #, python-brace-format msgid "|c{key}|R is already puppeted by another Account." -msgstr "" +msgstr "|c{key}|R est déjà contrôlé par un autre compte." -#: accounts/accounts.py:515 +#: accounts/accounts.py:516 msgid "Too many login failures; please try again in a few minutes." msgstr "" +"Trop d'échecs de tentatives de connexion; veuillez réessayer dans quelques " +"minutes." -#: accounts/accounts.py:528 accounts/accounts.py:789 +#: accounts/accounts.py:529 accounts/accounts.py:790 msgid "" "|rYou have been banned and cannot continue from here.\n" "If you feel this ban is in error, please email an admin.|x" msgstr "" +"|rVous avez été banni et ne pouvez plus continuer.\n" +"Si vous pensez que c'est une erreur, veuillez contacter un admin par e-mail.|" +"x" -#: accounts/accounts.py:540 +#: accounts/accounts.py:541 msgid "Username and/or password is incorrect." -msgstr "" +msgstr "Nom d'utilisateur et/ou mot de passe incorrect(s)." -#: accounts/accounts.py:547 +#: accounts/accounts.py:548 msgid "Too many authentication failures." -msgstr "" +msgstr "Trop d'échecs de tentatives de connexion." -#: accounts/accounts.py:760 +#: accounts/accounts.py:761 msgid "" "You are creating too many accounts. Please log into an existing account." msgstr "" +"Vous créez trop de comptes. Veuillez vous connecter à un compte existant." -#: accounts/accounts.py:805 +#: accounts/accounts.py:807 msgid "" "There was an error creating the Account. If this problem persists, contact " "an admin." msgstr "" +"Il y a eu une erreur lors de la création du compte. Si le problème persiste " +"veuillez contacter un admin." -#: accounts/accounts.py:839 accounts/accounts.py:1751 +#: accounts/accounts.py:843 accounts/accounts.py:1766 msgid "An error occurred. Please e-mail an admin if the problem persists." msgstr "" +"Une erreur s'est produite. Veuillez contacter un admin par e-mail si le " +"problème persiste." -#: accounts/accounts.py:866 +#: accounts/accounts.py:876 #, fuzzy msgid "Account being deleted." msgstr "Suppression du compte." -#: accounts/accounts.py:1417 accounts/accounts.py:1768 +#: accounts/accounts.py:1431 accounts/accounts.py:1783 #, python-brace-format msgid "|G{key} connected|n" -msgstr "" +msgstr "|G{key} s'est connecté(e)|n" -#: accounts/accounts.py:1424 accounts/accounts.py:1431 -#, fuzzy -#| msgid "The destination doesn't exist." +#: accounts/accounts.py:1438 accounts/accounts.py:1445 msgid "The Character does not exist." -msgstr "La destination est inconnue." +msgstr "Ce personnage n'existe pas." -#: accounts/accounts.py:1470 +#: accounts/accounts.py:1484 #, python-brace-format msgid "|R{key} disconnected{reason}|n" -msgstr "" +msgstr "|R{key} s'est déconnecté(e){reason}|n" -#: accounts/accounts.py:1704 +#: accounts/accounts.py:1719 msgid "Guest accounts are not enabled on this server." -msgstr "" +msgstr "Les comptes visiteurs ne sont pas actifs sur ce serveur." -#: accounts/accounts.py:1714 +#: accounts/accounts.py:1729 msgid "All guest accounts are in use. Please try again later." msgstr "" +"Tous les comptes visiteurs sont utilisés. Veuillez réessayer plus tard." -#: commands/cmdhandler.py:83 +#: commands/cmdhandler.py:84 msgid "" "\n" "An untrapped error occurred.\n" msgstr "" +"\n" +"Une erreur non capturée est survenue.\n" -#: commands/cmdhandler.py:86 +#: commands/cmdhandler.py:89 msgid "" "\n" "An untrapped error occurred. Please file a bug report detailing the steps to " "reproduce.\n" msgstr "" +"\n" +"Une erreur non capturée est survenue. Veuillez envoyer un rapport de bug " +"détaillant les étapes afin de la reproduire.\n" -#: commands/cmdhandler.py:92 +#: commands/cmdhandler.py:97 msgid "" "\n" "A cmdset merger-error occurred. This is often due to a syntax\n" "error in one of the cmdsets to merge.\n" msgstr "" +"\n" +"Une erreur de fusion de set de commandes est survenue. Cela est souvent dû\n" +"à une erreur de syntaxe lors de la fusion d'un set de commandes.\n" -#: commands/cmdhandler.py:96 +#: commands/cmdhandler.py:103 msgid "" "\n" "A cmdset merger-error occurred. Please file a bug report detailing the\n" "steps to reproduce.\n" msgstr "" +"\n" +"Une erreur de fusion de set de commandes est survenue. Veuillez envoyer un " +"rapport de bug\n" +"détaillant les étapes afin de la reproduire.\n" -#: commands/cmdhandler.py:103 +#: commands/cmdhandler.py:112 msgid "" "\n" "No command sets found! This is a critical bug that can have\n" "multiple causes.\n" msgstr "" +"\n" +"Aucun set de commandes trouvé! C'est un bug critique\n" +"qui peut avoir de multiple causes.\n" -#: commands/cmdhandler.py:107 +#: commands/cmdhandler.py:118 msgid "" "\n" "No command sets found! This is a sign of a critical bug. If\n" "disconnecting/reconnecting doesn't\" solve the problem, try to contact\n" "the server admin through\" some other means for assistance.\n" msgstr "" +"\n" +"Pas de set de commandes trouvé! C'est le signe d'un bug critique.\n" +"Si une déconnexion/reconnexion ne résous par le problème, essayez\n" +"de contacter l'admin du serveur afin d'obtenir de l'aide.\n" -#: commands/cmdhandler.py:115 +#: commands/cmdhandler.py:128 msgid "" "\n" "A command handler bug occurred. If this is not due to a local change,\n" "please file a bug report with the Evennia project, including the\n" "traceback and steps to reproduce.\n" msgstr "" +"\n" +"Un bug avec le command handler s'est produit. Si ce n'est pas dû à un " +"changement local,\n" +"veuillez envoyer un rapport de bug au projet Evennia incluant le traceback\n" +"et les étapes afin de le reproduire.\n" -#: commands/cmdhandler.py:120 +#: commands/cmdhandler.py:135 msgid "" "\n" "A command handler bug occurred. Please notify staff - they should\n" "likely file a bug report with the Evennia project.\n" msgstr "" +"\n" +"Un bug avec le command handler s'est produit. Veuillez notifier le staff.\n" +"Ils enverront probablement un rapport de bug au projet Evennia.\n" -#: commands/cmdhandler.py:127 +#: commands/cmdhandler.py:143 #, python-brace-format msgid "" "Command recursion limit ({recursion_limit}) reached for " "'{raw_cmdname}' ({cmdclass})." msgstr "" +"Limite de récursion de commande ({recursion_limit}) atteinte pour " +"'{raw_cmdname}' ({cmdclass})." -#: commands/cmdhandler.py:149 -#, fuzzy, python-brace-format -#| msgid "" -#| "{traceback}\n" -#| "Error loading cmdset '{path}'\n" -#| "(Traceback was logged {timestamp})" +#: commands/cmdhandler.py:165 +#, python-brace-format msgid "" "{traceback}\n" "{errmsg}\n" "(Traceback was logged {timestamp})." msgstr "" "{traceback}\n" -"Une erreur s'est produite lors du chargement du cmdset '{path}'\n" -"(Référence de l'erreur : {timestamp})" +"{errmsg}\n" +"(Le traceback a été écrit dans le log {timestamp})." -#: commands/cmdhandler.py:699 +#: commands/cmdhandler.py:715 msgid "There were multiple matches." msgstr "Il y a plusieurs correspondances possibles." -#: commands/cmdhandler.py:724 +#: commands/cmdhandler.py:740 #, python-brace-format msgid "Command '{command}' is not available." msgstr "La commande '{command}' n'est pas disponible." -#: commands/cmdhandler.py:734 +#: commands/cmdhandler.py:750 #, python-brace-format msgid " Maybe you meant {command}?" msgstr " Vouliez-vous dire {command} ?" -#: commands/cmdhandler.py:735 +#: commands/cmdhandler.py:751 msgid "or" msgstr "ou" -#: commands/cmdhandler.py:738 +#: commands/cmdhandler.py:754 msgid " Type \"help\" for help." msgstr " Tapez \"help\" pour obtenir de l'aide." @@ -194,42 +228,41 @@ msgid "" "(Traceback was logged {timestamp})" msgstr "" "{traceback}\n" -"Une erreur s'est produite lors du chargement du cmdset '{path}'\n" -"(Référence de l'erreur : {timestamp})" +"Une erreur s'est produite lors du chargement du set de commandes '{path}'\n" +"(Le traceback a été écrit dans le log : {timestamp})" #: commands/cmdsethandler.py:95 -#, fuzzy, python-brace-format +#, python-brace-format msgid "" "Error loading cmdset: No cmdset class '{classname}' in '{path}'.\n" "(Traceback was logged {timestamp})" msgstr "" -"\n" -"Une erreur s'est produite lors du chargement de cmdset : la classe cmdset " -"'{classname}' est introuvable dans {path}.\n" -"(Référence de l'erreur : {timestamp})" +"Une erreur s'est produite lors du chargement du set de commandes: la classe " +"cmdset '{classname}' est introuvable dans {path}.\n" +"(Le Traceback a été écrit dans le log: {timestamp})" #: commands/cmdsethandler.py:100 -#, fuzzy, python-brace-format +#, python-brace-format msgid "" "{traceback}\n" "SyntaxError encountered when loading cmdset '{path}'.\n" "(Traceback was logged {timestamp})" msgstr "" -"\n" -"Erreur de syntaxe lors du chargement de cmdset '{path}' : \"{error}\".\n" -"(Référence de l'erreur : {timestamp})" +"{traceback}\n" +"Erreur de syntaxe rencontrée lors du chargement de '{path}'.\n" +"(Le Traceback a été écrit dans le log {timestamp})" #: commands/cmdsethandler.py:106 -#, fuzzy, python-brace-format +#, python-brace-format msgid "" "{traceback}\n" "Compile/Run error when loading cmdset '{path}'.\n" "(Traceback was logged {timestamp})" msgstr "" -"\n" -"Erreur de compilation/exécution lors du chargement de cmdset '{path}' : " -"\"{error}\".\n" -"(Référence de l'erreur : {timestamp})" +"{traceback}\n" +"Erreur de compilation/exécution lors du chargement du set de commandes " +"'{path}'.\n" +"(Le Traceback a été écrit dans le log {timestamp})" #: commands/cmdsethandler.py:112 #, python-brace-format @@ -239,259 +272,274 @@ msgid "" "Replacing with fallback '{fallback_path}'.\n" msgstr "" "\n" -"Une erreur a été rencontrée lors du chargement du cmdset '{path}'.\n" -"Le cmdset '{fallback_path}' est utilisé en remplacement.\n" +"Une erreur a été rencontrée dans le set de commandes '{path}'.\n" +"Le set de commandes '{fallback_path}' est utilisé en remplacement.\n" #: commands/cmdsethandler.py:118 #, python-brace-format msgid "Fallback path '{fallback_path}' failed to generate a cmdset." -msgstr "Impossible de générer le cmdset de remplacement : '{fallback_path}'." +msgstr "" +"Impossible de générer le set de commandes de remplacement : " +"'{fallback_path}'." #: commands/cmdsethandler.py:188 commands/cmdsethandler.py:200 -#, fuzzy, python-brace-format +#, python-brace-format msgid "" "\n" "(Unsuccessfully tried '{path}')." msgstr "" "\n" -"(Essayé sans succès '%s.' + '%s.%s')." +"(Essayé sans succès'{path}')." #: commands/cmdsethandler.py:331 #, python-brace-format msgid "custom {mergetype} on cmdset '{cmdset}'" -msgstr "custom {mergetype} sur cmdset '{cmdset}'" +msgstr "{mergetype} personnalisé sur le set de commandes '{cmdset}'" #: commands/cmdsethandler.py:459 msgid "Only CmdSets can be added to the cmdsethandler!" -msgstr "Seuls des CmdSets peuvent être ajoutés au cmdsethandler !" +msgstr "Seuls des sets de commandes peuvent être ajoutés au cmdsethandler !" -#: locks/lockhandler.py:238 -#, fuzzy, python-brace-format -#| msgid "Lock: lock-function '%s' is not available." +#: locks/lockhandler.py:239 +#, python-brace-format msgid "Lock: lock-function '{lockfunc}' is not available." -msgstr "Verrou : lock-function '%s' n'est pas disponible." +msgstr "Verrou: la fonction de verrouilage '{lockfunc}' n'est pas disponible." -#: locks/lockhandler.py:259 -#, fuzzy, python-brace-format -#| msgid "Lock: definition '%s' has syntax errors." +#: locks/lockhandler.py:262 +#, python-brace-format msgid "Lock: definition '{lock_string}' has syntax errors." -msgstr "Verrou : la définition '%s' a des erreurs de syntaxe." +msgstr "" +"Verrou: la définition de '{lock_string}' comporte des erreurs de syntaxe." -#: locks/lockhandler.py:267 -#, fuzzy, python-brace-format -#| msgid "" -#| "LockHandler on %(obj)s: access type '%(access_type)s' changed from " -#| "'%(source)s' to '%(goal)s' " +#: locks/lockhandler.py:271 +#, python-brace-format msgid "" "LockHandler on {obj}: access type '{access_type}' changed from '{source}' to " "'{goal}' " msgstr "" -"Gestionnaire de verrous sur %(obj)s: type d'accès '%(access_type)s' a changé " -"de '%(source)s' à '%(goal)s'" +"Gestionnaire de verrou sur {obj}: type d'accès '{access_type}' changé de " +"'{source}' à '{goal}' " -#: locks/lockhandler.py:341 -#, fuzzy, python-brace-format +#: locks/lockhandler.py:347 +#, python-brace-format msgid "Lock: '{lockdef}' contains no colon (:)." -msgstr "Verrou : '%s' ne contient pas de deux points (:)." +msgstr "Verrou: '{lockdef}' manque deux points (:)." -#: locks/lockhandler.py:350 -#, fuzzy, python-brace-format +#: locks/lockhandler.py:356 +#, python-brace-format msgid "Lock: '{lockdef}' has no access_type (left-side of colon is empty)." msgstr "" -"Verrou : '%s' n'a pas de 'access_type' (il n'y a rien avant les deux points)." +"Verrou: '{lockdef}' ne possède pas de type d'accès (il n'y a rien avant les " +"deux points)." -#: locks/lockhandler.py:358 -#, fuzzy, python-brace-format +#: locks/lockhandler.py:364 +#, python-brace-format msgid "Lock: '{lockdef}' has mismatched parentheses." -msgstr "Verrou : '%s' a des parenthèses déséquilibrées." +msgstr "Verrou: '{lockdef}' comporte des parenthèses non concordantes." -#: locks/lockhandler.py:365 -#, fuzzy, python-brace-format +#: locks/lockhandler.py:371 +#, python-brace-format msgid "Lock: '{lockdef}' has no valid lock functions." -msgstr "Verrou : '%s' n'a pas de lock-function valide." +msgstr "Verrou: '{lockdef}' ne possède pas de fonction de verrouillage valide." -#: objects/objects.py:871 -#, fuzzy, python-brace-format -#| msgid "Couldn't perform move ('%s'). Contact an admin." +#: objects/objects.py:891 +#, python-brace-format msgid "Couldn't perform move ({err}). Contact an admin." msgstr "" -"Impossible de se déplacer vers ('%s'). Veuillez contacter un administrateur." +"Impossible d'effectuer le déplacement ({err}). Veuillez contacter un admin." -#: objects/objects.py:881 +#: objects/objects.py:901 msgid "The destination doesn't exist." -msgstr "La destination est inconnue." +msgstr "La destination n'existe pas." -#: objects/objects.py:974 -#, fuzzy, python-brace-format -#| msgid "Could not find default home '(#%d)'." +#: objects/objects.py:993 +#, python-brace-format msgid "Could not find default home '(#{dbid})'." -msgstr "" -"Impossible de trouver la salle de départ (default home) par défaut : '#%d'." +msgstr "Impossible de trouver le foyer par défaut '(#{dbid})'." -#: objects/objects.py:988 +#: objects/objects.py:1007 msgid "Something went wrong! You are dumped into nowhere. Contact an admin." msgstr "" "Quelque chose a mal tourné ! Vous vous trouvez au milieu de nulle part. " -"Veuillez contacter un administrateur." +"Veuillez contacter un admin." -#: objects/objects.py:1138 -#, fuzzy, python-brace-format -#| msgid "Your character %s has been destroyed." +#: objects/objects.py:1159 +#, python-brace-format msgid "Your character {key} has been destroyed." -msgstr "Votre personnage %s a été détruit." +msgstr "Votre personnage {key} a été détruit." -#: objects/objects.py:1546 +#: objects/objects.py:1570 #, python-brace-format msgid "You now have {name} in your possession." -msgstr "" +msgstr "Vous avez maintenant {name} en votre possession." -#: objects/objects.py:1555 +#: objects/objects.py:1580 #, python-brace-format msgid "{object} arrives to {destination} from {origin}." msgstr "" -#: objects/objects.py:1557 +#: objects/objects.py:1582 #, python-brace-format msgid "{object} arrives to {destination}." msgstr "" -#: objects/objects.py:2165 +#: objects/objects.py:2279 msgid "Invalid character name." -msgstr "" +msgstr "Nom de personnage invalide." -#: objects/objects.py:2184 +#: objects/objects.py:2298 msgid "There are too many characters associated with this account." -msgstr "" +msgstr "Il y a trop de personnages associés à ce compte." -#: objects/objects.py:2210 -#, fuzzy -#| msgid "This is User #1." +#: objects/objects.py:2324 msgid "This is a character." -msgstr "C'est l'utilisateur #1." +msgstr "C'est un personnage." -#: objects/objects.py:2296 +#: objects/objects.py:2413 #, python-brace-format msgid "|r{obj} has no location and no home is set.|n" -msgstr "" +msgstr "|r{obj} n'a pas de localisation et aucun foyer n'est déterminé.|n" -#: objects/objects.py:2315 +#: objects/objects.py:2431 #, python-brace-format msgid "" "\n" "You become |c{name}|n.\n" msgstr "" +"\n" +"Vous contrôlez maintenant |c{name}|n.\n" -#: objects/objects.py:2319 +#: objects/objects.py:2436 #, python-brace-format msgid "{name} has entered the game." -msgstr "" +msgstr "{name} est entré(e) dans le jeu." -#: objects/objects.py:2343 +#: objects/objects.py:2462 #, python-brace-format msgid "{name} has left the game." -msgstr "" +msgstr "{name} est sorti(e) du jeu." -#: objects/objects.py:2461 -#, fuzzy -#| msgid "This is User #1." +#: objects/objects.py:2581 msgid "This is a room." -msgstr "C'est l'utilisateur #1." +msgstr "C'est une pièce." -#: objects/objects.py:2667 -#, fuzzy -#| msgid "This is User #1." +#: objects/objects.py:2788 msgid "This is an exit." -msgstr "C'est l'utilisateur #1." +msgstr "C'est une sortie." -#: objects/objects.py:2764 +#: objects/objects.py:2885 msgid "You cannot go there." -msgstr "" +msgstr "Vous ne pouvez pas aller là." -#: prototypes/prototypes.py:57 +#: prototypes/prototypes.py:55 msgid "Error" -msgstr "" +msgstr "Erreur" -#: prototypes/prototypes.py:58 +#: prototypes/prototypes.py:56 msgid "Warning" -msgstr "" +msgstr "Avertissement" -#: prototypes/prototypes.py:263 +#: prototypes/prototypes.py:390 msgid "Prototype requires a prototype_key" -msgstr "" +msgstr "Le prototype requiert prototype_key" -#: prototypes/prototypes.py:271 prototypes/prototypes.py:339 +#: prototypes/prototypes.py:398 prototypes/prototypes.py:467 +#: prototypes/prototypes.py:1085 #, python-brace-format msgid "{protkey} is a read-only prototype (defined as code in {module})." msgstr "" +"{protkey} est un prototype en lecture seule (définit comme code dans " +"{module})." -#: prototypes/prototypes.py:346 +#: prototypes/prototypes.py:400 prototypes/prototypes.py:469 +#: prototypes/prototypes.py:1087 +#, python-brace-format +msgid "{protkey} is a read-only prototype (passed directly as a dict)." +msgstr "" +"{protkey} est un prototype en lecture seule (passé directement comme un " +"dictionnaire)." + +#: prototypes/prototypes.py:476 #, python-brace-format msgid "Prototype {prototype_key} was not found." -msgstr "" +msgstr "Le prototype {prototype_key} n'a pas été trouvé." -#: prototypes/prototypes.py:353 +#: prototypes/prototypes.py:484 #, python-brace-format msgid "" "{caller} needs explicit 'edit' permissions to delete prototype " "{prototype_key}." msgstr "" +"{caller} a besoin de la permission explicite 'edit' pour supprimer le " +"prototype {prototype_key}." -#: prototypes/prototypes.py:455 +#: prototypes/prototypes.py:604 #, python-brace-format -msgid "Found {num} matching prototypes {module_prototypes}." -msgstr "" +msgid "Found {num} matching prototypes among {module_prototypes}." +msgstr "Trouvé {num} prototypes correspondants parmi {module_prototypes}." -#: prototypes/prototypes.py:615 +#: prototypes/prototypes.py:764 msgid "No prototypes found." -msgstr "" +msgstr "Aucun prototype trouvé." -#: prototypes/prototypes.py:666 +#: prototypes/prototypes.py:815 msgid "Prototype lacks a 'prototype_key'." -msgstr "" +msgstr "Le prototype n'a pas de 'prototype_key'." -#: prototypes/prototypes.py:675 +#: prototypes/prototypes.py:824 #, python-brace-format msgid "Prototype {protkey} requires `typeclass` or 'prototype_parent'." -msgstr "" +msgstr "Le prototype {protkey} requiert `typeclass` ou 'prototype_parent'." -#: prototypes/prototypes.py:680 +#: prototypes/prototypes.py:831 #, python-brace-format msgid "" "Prototype {protkey} can only be used as a mixin since it lacks 'typeclass' " "or 'prototype_parent' keys." msgstr "" +"Le prototype {protkey} ne peut être utilisé qu'en tant que mixin car il n'a " +"pas de clé 'typeclass' ou 'prototype_parent'." -#: prototypes/prototypes.py:689 +#: prototypes/prototypes.py:842 #, python-brace-format msgid "" "{err}: Prototype {protkey} is based on typeclass {typeclass}, which could " "not be imported!" msgstr "" +"{err} : Le prototype {protkey} est basé sur la typeclass {typeclass}, qui " +"n'a pas pu être importée !" -#: prototypes/prototypes.py:699 +#: prototypes/prototypes.py:861 #, python-brace-format msgid "Prototype {protkey} tries to parent itself." -msgstr "" +msgstr "Le prototype {protkey} essaie d'être son propre parent." -#: prototypes/prototypes.py:704 +#: prototypes/prototypes.py:867 #, python-brace-format -msgid "Prototype {protkey}'s prototype_parent '{parent}' was not found." +msgid "" +"Prototype {protkey}'s `prototype_parent` (named '{parent}') was not found." msgstr "" +"Le `prototype_parent` du prototype {protkey} (nommé '{parent}') n'a pas été " +"trouvé." -#: prototypes/prototypes.py:709 +#: prototypes/prototypes.py:875 #, python-brace-format msgid "{protkey} has infinite nesting of prototypes." -msgstr "" +msgstr "{protkey} a une imbrication infinie de prototypes." -#: prototypes/prototypes.py:729 +#: prototypes/prototypes.py:900 #, python-brace-format msgid "" "Prototype {protkey} has no `typeclass` defined anywhere in its parent\n" " chain. Add `typeclass`, or a `prototype_parent` pointing to a prototype " "with a typeclass." msgstr "" +"Le prototype {protkey} n'a pas de `typeclass` défini dans sa chaîne " +"parentale.\n" +"Ajoutez une `typeclass`, ou un `prototype_parent` pointant vers un prototype " +"avec une typeclass." -#: prototypes/spawner.py:473 +#: prototypes/spawner.py:497 #, python-brace-format msgid "" "Diff contains non-dicts that are not on the form (old, new, action_to_take): " @@ -500,79 +548,75 @@ msgstr "" #: scripts/scripthandler.py:51 #, fuzzy, python-brace-format -#| msgid "" -#| "\n" -#| " '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s" msgid "" "\n" " '{key}' ({next_repeat}/{interval}, {repeats} repeats): {desc}" msgstr "" "\n" -" '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s répète) : %(desc)s" +" '{key}' ({next_repeat}/{interval}, {repeats} répète): {desc}" -#: scripts/scripts.py:329 -#, fuzzy, python-brace-format -#| msgid "" -#| "Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error " -#| "'%(err)s'." +#: scripts/scripts.py:344 +#, python-brace-format msgid "Script {key}(#{dbid}) of type '{name}': at_repeat() error '{err}'." msgstr "" -"Le script %(key)s(#%(dbid)s) de type '%(cname)s' a rencontré une erreur " -"durant at_repeat() : '%(err)s'." +"Le script {key}(#{dbid}) de type '{name}': a rencontré une erreur durant " +"at_repeat(): '{err}'." -#: server/initial_setup.py:28 -#, fuzzy +#: server/initial_setup.py:29 msgid "" "\n" "Welcome to your new |wEvennia|n-based game! Visit https://www.evennia.com if " "you need\n" "help, want to contribute, report issues or just join the community.\n" -"As Account #1 you can create a demo/tutorial area with '|wbatchcommand " -"tutorial_world.build|n'.\n" +"\n" +"As a privileged user, write |wbatchcommand tutorial_world.build|n to build\n" +"tutorial content. Once built, try |wintro|n for starting help and |wtutorial|" +"n to\n" +"play the demo game.\n" msgstr "" "\n" "Bienvenue dans votre nouveau jeu basé sur |wEvennia|n ! Visitez le site Web\n" "http://www.evennia.com si vous avez besoin d'aide, pour contribuer au " "projet,\n" "afin de rapporter des bugs ou faire partie de la communauté.\n" -"En tant que premier personnage (#1), vous pouvez créer une zone de\n" +"En tant que super utilisateur (#1), vous pouvez créer une zone de\n" "démo/tutoriel en entrant la commande |w@batchcommand tutorial_world.build|" "n.\n" -" " -#: server/initial_setup.py:92 +#: server/initial_setup.py:108 msgid "This is User #1." msgstr "C'est l'utilisateur #1." -#: server/initial_setup.py:108 +#: server/initial_setup.py:128 msgid "Limbo" msgstr "Limbes" -#: server/portal/portalsessionhandler.py:40 +#: server/portal/portalsessionhandler.py:41 #, python-brace-format msgid "" -"{servername} DoS protection is active. You are queued to connect in {num} " +"{servername} DoS protection is active.You are queued to connect in {num} " "seconds ..." msgstr "" +"La protection DoS de {servername} est active. Vous êtes en attente de " +"connexion dans {num} secondes ..." -#: server/server.py:152 -#, fuzzy +#: server/server.py:153 msgid "idle timeout exceeded" -msgstr "Délai d'inactivité dépassé, déconnexion." +msgstr "délai d'inactivité dépassé" -#: server/sessionhandler.py:42 +#: server/sessionhandler.py:43 msgid "Your client sent an incorrect UTF-8 sequence." -msgstr "" +msgstr "Votre client a envoyé une séquence UTF-8 incorrecte." -#: server/sessionhandler.py:399 +#: server/sessionhandler.py:407 msgid " ... Server restarted." msgstr " ... Serveur redémarré." -#: server/sessionhandler.py:623 +#: server/sessionhandler.py:631 msgid "Logged in from elsewhere. Disconnecting." msgstr "Connexion d'une autre session. Déconnexion de celle-ci." -#: server/sessionhandler.py:652 +#: server/sessionhandler.py:659 msgid "Idle timeout exceeded, disconnecting." msgstr "Délai d'inactivité dépassé, déconnexion." @@ -580,29 +624,28 @@ msgstr "Délai d'inactivité dépassé, déconnexion." msgid "" "Too many failed attempts; you must wait a few minutes before trying again." msgstr "" +"Trop de tentatives ont échoué ; vous devez attendre quelques minutes avant " +"de réessayer." #: server/validators.py:31 msgid "Sorry, that username is reserved." -msgstr "" +msgstr "Désolé, ce nom d'utilisateur est réservé." #: server/validators.py:38 msgid "Sorry, that username is already taken." -msgstr "" +msgstr "Désolé, ce nom d'utilisateur est déjà pris." #: server/validators.py:88 -#, fuzzy, python-brace-format -#| msgid "" -#| "%s From a terminal client, you can also use a phrase of multiple words if " -#| "you enclose the password in double quotes." +#, python-brace-format msgid "" "{policy} From a terminal client, you can also use a phrase of multiple words " "if you enclose the password in double quotes." msgstr "" -"%s Depuis votre client, vous pouvez également préciser une phrase contenant " -"plusieurs mots séparés par un espace, dès lors que cette phrase est entourée " -"de guillemets." +"{policy} Depuis votre client, vous pouvez également préciser une phrase " +"contenant plusieurs mots séparés par un espace, dès lors que cette phrase " +"est entourée de guillemets." -#: utils/eveditor.py:67 +#: utils/eveditor.py:68 msgid "" "\n" " - any non-command is appended to the end of the buffer.\n" @@ -645,7 +688,7 @@ msgid "" " :echo - turn echoing of the input on/off (helpful for some clients)\n" msgstr "" -#: utils/eveditor.py:105 +#: utils/eveditor.py:108 msgid "" "\n" " Legend:\n" @@ -654,7 +697,7 @@ msgid "" " - longer string, usually not needing quotes.\n" msgstr "" -#: utils/eveditor.py:112 +#: utils/eveditor.py:117 msgid "" "\n" " :! - Execute code buffer without saving\n" @@ -663,7 +706,7 @@ msgid "" " := - Switch automatic indentation on/off\n" msgstr "" -#: utils/eveditor.py:121 +#: utils/eveditor.py:128 #, python-brace-format msgid "" "\n" @@ -672,7 +715,7 @@ msgid "" "|rBuffer load function error. Could not load initial data.|n\n" msgstr "" -#: utils/eveditor.py:127 +#: utils/eveditor.py:136 #, python-brace-format msgid "" "\n" @@ -681,19 +724,19 @@ msgid "" "|rSave function returned an error. Buffer not saved.|n\n" msgstr "" -#: utils/eveditor.py:133 +#: utils/eveditor.py:143 msgid "|rNo save function defined. Buffer cannot be saved.|n" msgstr "" -#: utils/eveditor.py:135 +#: utils/eveditor.py:145 msgid "No changes need saving" -msgstr "" +msgstr "Aucune modification ne doit être sauvegardée" -#: utils/eveditor.py:136 +#: utils/eveditor.py:146 msgid "Exited editor." -msgstr "" +msgstr "Sortie de l'éditeur:" -#: utils/eveditor.py:138 +#: utils/eveditor.py:149 #, python-brace-format msgid "" "\n" @@ -702,7 +745,7 @@ msgid "" "|rQuit function gave an error. Skipping.|n\n" msgstr "" -#: utils/eveditor.py:144 +#: utils/eveditor.py:157 #, python-brace-format msgid "" "\n" @@ -713,243 +756,238 @@ msgid "" "an eventual server reload - so save often!)|n\n" msgstr "" -#: utils/eveditor.py:153 +#: utils/eveditor.py:167 msgid "" "EvEditor persistent-mode error. Commonly, this is because one or more of the " "EvEditor callbacks could not be pickled, for example because it's a class " "method or is defined inside another function." msgstr "" -#: utils/eveditor.py:159 +#: utils/eveditor.py:173 msgid "Nothing to undo." msgstr "" -#: utils/eveditor.py:160 +#: utils/eveditor.py:174 msgid "Nothing to redo." msgstr "" -#: utils/eveditor.py:161 +#: utils/eveditor.py:175 msgid "Undid one step." msgstr "" -#: utils/eveditor.py:162 +#: utils/eveditor.py:176 msgid "Redid one step." msgstr "" -#: utils/eveditor.py:480 +#: utils/eveditor.py:494 msgid "Single ':' added to buffer." msgstr "" -#: utils/eveditor.py:495 +#: utils/eveditor.py:509 msgid "Save before quitting?" -msgstr "" +msgstr "Sauver avant de quitter ?" -#: utils/eveditor.py:510 +#: utils/eveditor.py:524 msgid "Reverted all changes to the buffer back to original state." msgstr "" +"Retour à l'état initial de toutes les modifications apportées au tampon." -#: utils/eveditor.py:515 +#: utils/eveditor.py:529 #, python-brace-format msgid "Deleted {string}." msgstr "" -#: utils/eveditor.py:520 +#: utils/eveditor.py:534 msgid "You must give a search word to delete." -msgstr "" +msgstr "Vous devez donner un mot de recherche à supprimer." -#: utils/eveditor.py:525 +#: utils/eveditor.py:540 #, python-brace-format msgid "Removed {arg1} for lines {l1}-{l2}." msgstr "" -#: utils/eveditor.py:528 +#: utils/eveditor.py:546 #, python-brace-format msgid "Removed {arg1} for {line}." msgstr "" -#: utils/eveditor.py:544 +#: utils/eveditor.py:562 #, python-brace-format msgid "Cleared {nlines} lines from buffer." msgstr "" -#: utils/eveditor.py:549 +#: utils/eveditor.py:567 #, python-brace-format msgid "{line}, {cbuf} yanked." msgstr "" -#: utils/eveditor.py:556 +#: utils/eveditor.py:574 #, python-brace-format msgid "{line}, {cbuf} cut." msgstr "" -#: utils/eveditor.py:560 +#: utils/eveditor.py:578 msgid "Copy buffer is empty." -msgstr "" +msgstr "Le tampon de copie est vide." -#: utils/eveditor.py:564 +#: utils/eveditor.py:583 #, python-brace-format msgid "Pasted buffer {cbuf} to {line}." msgstr "" -#: utils/eveditor.py:570 +#: utils/eveditor.py:591 msgid "You need to enter a new line and where to insert it." -msgstr "" +msgstr "Vous devez saisir une nouvelle ligne et indiquer où l'insérer." -#: utils/eveditor.py:574 +#: utils/eveditor.py:596 #, python-brace-format msgid "Inserted {num} new line(s) at {line}." msgstr "" -#: utils/eveditor.py:580 +#: utils/eveditor.py:604 msgid "You need to enter a replacement string." -msgstr "" +msgstr "Vous devez saisir une chaîne de remplacement." -#: utils/eveditor.py:584 +#: utils/eveditor.py:609 #, python-brace-format msgid "Replaced {num} line(s) at {line}." msgstr "" -#: utils/eveditor.py:589 +#: utils/eveditor.py:616 msgid "You need to enter text to insert." -msgstr "" +msgstr "Vous devez saisir le texte à insérer." -#: utils/eveditor.py:597 +#: utils/eveditor.py:624 #, python-brace-format msgid "Inserted text at beginning of {line}." msgstr "" -#: utils/eveditor.py:601 +#: utils/eveditor.py:628 msgid "You need to enter text to append." msgstr "" -#: utils/eveditor.py:609 +#: utils/eveditor.py:636 #, python-brace-format msgid "Appended text to end of {line}." msgstr "" -#: utils/eveditor.py:614 +#: utils/eveditor.py:641 msgid "You must give a search word and something to replace it with." msgstr "" +"Vous devez donner un mot de recherche et quelque chose pour le remplacer." -#: utils/eveditor.py:620 +#: utils/eveditor.py:647 #, python-brace-format msgid "Search-replaced {arg1} -> {arg2} for lines {l1}-{l2}." msgstr "" -#: utils/eveditor.py:625 +#: utils/eveditor.py:653 #, python-brace-format msgid "Search-replaced {arg1} -> {arg2} for {line}." msgstr "" -#: utils/eveditor.py:648 +#: utils/eveditor.py:677 #, python-brace-format msgid "Flood filled lines {l1}-{l2}." msgstr "" -#: utils/eveditor.py:651 +#: utils/eveditor.py:679 #, python-brace-format msgid "Flood filled {line}." msgstr "" -#: utils/eveditor.py:673 +#: utils/eveditor.py:701 msgid "Valid justifications are" msgstr "" -#: utils/eveditor.py:681 -#, python-brace-format -msgid "{align}-justified lines {l1}-{l2}." -msgstr "" - -#: utils/eveditor.py:684 -#, python-brace-format -msgid "{align}-justified {line}." -msgstr "" - -#: utils/eveditor.py:696 -#, python-brace-format -msgid "Indented lines {l1}-{l2}." -msgstr "" - -#: utils/eveditor.py:698 -#, python-brace-format -msgid "Indented {line}." -msgstr "" - -#: utils/eveditor.py:707 -#, python-brace-format -msgid "Removed left margin (dedented) lines {l1}-{l2}." -msgstr "" - #: utils/eveditor.py:710 #, python-brace-format -msgid "Removed left margin (dedented) {line}." +msgid "{align}-justified lines {l1}-{l2}." msgstr "" -#: utils/eveditor.py:718 +#: utils/eveditor.py:716 +#, python-brace-format +msgid "{align}-justified {line}." +msgstr "" + +#: utils/eveditor.py:728 +#, python-brace-format +msgid "Indented lines {l1}-{l2}." +msgstr "Lignes indentées {l1}-{l2}." + +#: utils/eveditor.py:730 +#, python-brace-format +msgid "Indented {line}." +msgstr "Indentée {line}." + +#: utils/eveditor.py:740 +#, python-brace-format +msgid "Removed left margin (dedented) lines {l1}-{l2}." +msgstr "Suppression de la marge gauche (dédentée) lignes {l1}-{l2}." + +#: utils/eveditor.py:745 +#, python-brace-format +msgid "Removed left margin (dedented) {line}." +msgstr "Suppression de la marge gauche (dédentée) {line}." + +#: utils/eveditor.py:753 #, python-brace-format msgid "Echo mode set to {mode}" -msgstr "" +msgstr "Mode d'écho réglé sur {mode}" -#: utils/eveditor.py:723 utils/eveditor.py:736 utils/eveditor.py:749 -#: utils/eveditor.py:760 +#: utils/eveditor.py:758 utils/eveditor.py:773 utils/eveditor.py:788 +#: utils/eveditor.py:799 msgid "This command is only available in code editor mode." -msgstr "" +msgstr "Cette commande n'est disponible qu'en mode éditeur de code." -#: utils/eveditor.py:731 +#: utils/eveditor.py:766 #, python-brace-format msgid "Decreased indentation: new indentation is {indent}." -msgstr "" +msgstr "Diminution de l'indentation : la nouvelle indentation est {indent}." -#: utils/eveditor.py:734 utils/eveditor.py:747 +#: utils/eveditor.py:771 utils/eveditor.py:786 msgid "|rManual indentation is OFF.|n Use := to turn it on." -msgstr "" +msgstr "|rL'indentation manuelle est désactivée.|n Utilisez := pour l'activer." -#: utils/eveditor.py:744 +#: utils/eveditor.py:781 #, python-brace-format msgid "Increased indentation: new indentation is {indent}." -msgstr "" +msgstr "Augmentation de l'indentation : la nouvelle indentation est {indent}." -#: utils/eveditor.py:756 +#: utils/eveditor.py:795 msgid "Auto-indentation turned on." -msgstr "" +msgstr "Auto-indentation activée." -#: utils/eveditor.py:758 +#: utils/eveditor.py:797 msgid "Auto-indentation turned off." -msgstr "" +msgstr "Auto-indentation désactivée." -#: utils/eveditor.py:913 -msgid "|rNote: input buffer was converted to a string.|n" -msgstr "" - -#: utils/eveditor.py:1050 +#: utils/eveditor.py:1093 #, python-brace-format msgid "Line Editor [{name}]" msgstr "" -#: utils/eveditor.py:1058 +#: utils/eveditor.py:1101 msgid "(:h for help)" msgstr "" #: utils/evmenu.py:302 -#, fuzzy, python-brace-format -#| msgid "" -#| "Menu node '{nodename}' is either not implemented or caused an error. Make " -#| "another choice." +#, python-brace-format msgid "" "Menu node '{nodename}' is either not implemented or caused an error. Make " "another choice or try 'q' to abort." msgstr "" -"Ce choix '{nodename}' n'est pas implémenté, ou bien a créé une erreur. Faies " -"un autre choix." +"Le nœud de menu '{nodename}' n'est pas implémenté ou a provoqué une erreur. " +"Faites un autre choix ou essayez 'q' pour abandonner." #: utils/evmenu.py:305 #, python-brace-format msgid "Error in menu node '{nodename}'." -msgstr "Une erreur s'est produite dans le choix '{nodename}'." +msgstr "Erreur dans le nœud de menu '{nodename}'." #: utils/evmenu.py:306 msgid "No description." -msgstr "Description non renseignée." +msgstr "Aucune description." #: utils/evmenu.py:307 msgid "Commands: , help, quit" @@ -967,185 +1005,208 @@ msgstr "Utilisez une des commandes : help, quit" msgid "Commands: help" msgstr "Utilisez la commande : help" -#: utils/evmenu.py:311 utils/evmenu.py:1842 +#: utils/evmenu.py:311 utils/evmenu.py:1861 msgid "Choose an option or try 'help'." msgstr "Choisissez une option ou entrez la commande 'help'." -#: utils/evmenu.py:1383 +#: utils/evmenu.py:1387 msgid "|rInvalid choice.|n" -msgstr "" +msgstr "|rChoix invalide.|n" -#: utils/evmenu.py:1441 +#: utils/evmenu.py:1451 msgid "|Wcurrent|n" msgstr "" -#: utils/evmenu.py:1449 +#: utils/evmenu.py:1459 msgid "|wp|Wrevious page|n" msgstr "" -#: utils/evmenu.py:1456 +#: utils/evmenu.py:1466 msgid "|wn|Wext page|n" msgstr "" -#: utils/evmenu.py:1690 +#: utils/evmenu.py:1701 msgid "Aborted." -msgstr "" +msgstr "Abandonné." -#: utils/evmenu.py:1713 +#: utils/evmenu.py:1724 msgid "|rError in ask_yes_no. Choice not confirmed (report to admin)|n" -msgstr "" +msgstr "|rErreur dans ask_yes_no. Choix non confirmé (signaler à l'admin)|n" #: utils/evmore.py:235 -msgid "Exited |wmore|n pager." -msgstr "" +msgid "|xExited pager.|n" +msgstr "|xSortie du pager.|n" #: utils/optionhandler.py:138 utils/optionhandler.py:162 msgid "Option not found!" -msgstr "" +msgstr "Option non trouvée !" #: utils/optionhandler.py:159 msgid "Option field blank!" -msgstr "" +msgstr "Champ d'option vide !" -#: utils/optionhandler.py:164 -#, fuzzy -#| msgid "There were multiple matches." +#: utils/optionhandler.py:165 msgid "Multiple matches:" -msgstr "Il y a plusieurs correspondances possibles." +msgstr "Plusieurs correspondances possibles:" -#: utils/optionhandler.py:166 +#: utils/optionhandler.py:165 msgid "Please be more specific." +msgstr "Veuillez être plus précis." + +#: utils/utils.py:2115 +#, python-brace-format +msgid "" +"{obj}.{handlername} is a handler and can't be set directly. To add values, " +"use `{obj}.{handlername}.add()` instead." msgstr "" +"{obj}.{handlername} est un handler et ne peut pas être défini directement. " +"Pour ajouter des valeurs, utilisez `{obj}.{handlername}.add()` à la place." -#: utils/utils.py:2219 -#, fuzzy, python-brace-format -#| msgid "Could not find '%s'." +#: utils/utils.py:2125 +#, python-brace-format +msgid "" +"{obj}.{handlername} is a handler and can't be deleted directly. To remove " +"values, use `{obj}.{handlername}.remove()` instead." +msgstr "" +"{obj}.{handlername} est un handler et ne peut pas être supprimé directement. " +"Pour supprimer des valeurs, utilisez plutôt `{obj}.{handlername}.remove()` ." + +#: utils/utils.py:2266 +#, python-brace-format msgid "Could not find '{query}'." -msgstr "Impossible de trouver '%s'." +msgstr "Impossible de trouver '{query}'." -#: utils/utils.py:2226 -#, fuzzy, python-brace-format -#| msgid "More than one match for '%s' (please narrow target):\n" +#: utils/utils.py:2273 +#, python-brace-format msgid "More than one match for '{query}' (please narrow target):\n" -msgstr "Plus d'une possibilité pour '%s' (veuillez préciser) :\n" +msgstr "" +"Plus d'une correspondance pour '{query}' (veuillez préciser la cible):\n" +"\n" #: utils/validatorfuncs.py:25 #, python-brace-format msgid "Input could not be converted to text ({err})" -msgstr "" +msgstr "L'entrée n'a pas pu être convertie en texte ({err})" #: utils/validatorfuncs.py:34 #, python-brace-format msgid "Nothing entered for a {option_key}!" -msgstr "" +msgstr "Rien n'a été saisi pour une {option_key} !" -#: utils/validatorfuncs.py:37 +#: utils/validatorfuncs.py:38 #, python-brace-format msgid "'{entry}' is not a valid {option_key}." -msgstr "" +msgstr "'{entry}' n'est pas une {option_key} valide." -#: utils/validatorfuncs.py:62 utils/validatorfuncs.py:223 +#: utils/validatorfuncs.py:63 utils/validatorfuncs.py:236 #, python-brace-format msgid "No {option_key} entered!" -msgstr "" +msgstr "Aucune {option_key} n'a été saisie !" -#: utils/validatorfuncs.py:71 +#: utils/validatorfuncs.py:72 #, python-brace-format msgid "Timezone string '{acct_tz}' is not a valid timezone ({err})" msgstr "" +"La chaîne de fuseau horaire '{acct_tz}' n'est pas un fuseau horaire valide " +"({err})" -#: utils/validatorfuncs.py:88 utils/validatorfuncs.py:96 +#: utils/validatorfuncs.py:89 utils/validatorfuncs.py:97 #, python-brace-format msgid "{option_key} must be entered in a 24-hour format such as: {timeformat}" msgstr "" +"{option_key} doit être saisie dans un format de 24 heures tel que : " +"{timeformat}" -#: utils/validatorfuncs.py:140 +#: utils/validatorfuncs.py:141 #, python-brace-format msgid "Could not convert section '{interval}' to a {option_key}." -msgstr "" +msgstr "Impossible de convertir la section '{interval}' en {option_key}." -#: utils/validatorfuncs.py:150 +#: utils/validatorfuncs.py:153 #, python-brace-format msgid "That {option_key} is in the past! Must give a Future datetime!" -msgstr "" +msgstr "Cette {option_key} est dans le passé ! Doit donner une date future !" -#: utils/validatorfuncs.py:157 +#: utils/validatorfuncs.py:163 #, python-brace-format msgid "Must enter a whole number for {option_key}!" -msgstr "" +msgstr "Vous devez entrer un nombre entier pour {option_key} !" -#: utils/validatorfuncs.py:162 +#: utils/validatorfuncs.py:169 #, python-brace-format msgid "Could not convert '{entry}' to a whole number for {option_key}!" msgstr "" +"Impossible de convertir '{entry}' en un nombre entier pour {option_key} !" -#: utils/validatorfuncs.py:171 +#: utils/validatorfuncs.py:180 #, python-brace-format msgid "Must enter a whole number greater than 0 for {option_key}!" -msgstr "" +msgstr "Vous devez entrer un nombre entier supérieur à 0 pour {option_key} !" -#: utils/validatorfuncs.py:179 +#: utils/validatorfuncs.py:191 #, python-brace-format msgid "{option_key} must be a whole number greater than or equal to 0!" -msgstr "" +msgstr "{option_key} doit être un nombre entier supérieur ou égal à 0 !" -#: utils/validatorfuncs.py:197 +#: utils/validatorfuncs.py:210 #, python-brace-format msgid "Must enter a true/false input for {option_key}. Accepts {alternatives}." msgstr "" - -#: utils/validatorfuncs.py:227 -#, python-brace-format -msgid "That matched: {matches}. Please be more specific!" -msgstr "" - -#: utils/validatorfuncs.py:231 -#, python-brace-format -msgid "Could not find timezone '{entry}' for {option_key}!" -msgstr "" - -#: utils/validatorfuncs.py:237 -msgid "Email address field empty!" -msgstr "" +"Doit saisir une entrée vrai/faux pour {option_key}. Accepte les " +"{alternatives}." #: utils/validatorfuncs.py:240 #, python-brace-format -msgid "That isn't a valid {option_key}!" -msgstr "" +msgid "That matched: {matches}. Please be more specific!" +msgstr "Correspondances: {matches}. Veuillez être plus précis!" #: utils/validatorfuncs.py:247 #, python-brace-format +msgid "Could not find timezone '{entry}' for {option_key}!" +msgstr "Impossible de trouver le fuseau horaire '{entry}' pour {option_key} !" + +#: utils/validatorfuncs.py:255 +msgid "Email address field empty!" +msgstr "Le champ de l'adresse électronique est vide !" + +#: utils/validatorfuncs.py:258 +#, python-brace-format +msgid "That isn't a valid {option_key}!" +msgstr "Ce n'est pas une {option_key} valide !" + +#: utils/validatorfuncs.py:265 +#, python-brace-format msgid "No {option_key} entered to set!" -msgstr "" +msgstr "Aucune {option_key} n'a été saisie pour le réglage !" -#: utils/validatorfuncs.py:251 +#: utils/validatorfuncs.py:269 msgid "Must enter an access type!" -msgstr "" +msgstr "Vous devez entrer un type d'accès !" -#: utils/validatorfuncs.py:254 +#: utils/validatorfuncs.py:273 #, python-brace-format msgid "Access type must be one of: {alternatives}" -msgstr "" +msgstr "Le type d'accès doit être l'un des suivants : {alternatives}" -#: utils/validatorfuncs.py:257 +#: utils/validatorfuncs.py:278 msgid "Lock func not entered." -msgstr "" +msgstr "La fonction de verrouillage n'a pas été saisie." #: web/templates/admin/app_list.html:19 msgid "Add" -msgstr "" +msgstr "Ajouter" #: web/templates/admin/app_list.html:26 msgid "View" -msgstr "" +msgstr "Voir" #: web/templates/admin/app_list.html:28 msgid "Change" -msgstr "" +msgstr "Changer" #: web/templates/admin/app_list.html:39 msgid "You don’t have permission to view or edit anything." -msgstr "" +msgstr "Vous n'avez pas la permission de voir ou de modifier quoi que ce soit." #~ msgid " : {current}" #~ msgstr " : {current}" From 6f6ceb231c1b35adf607477af26100482035dc35 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sat, 19 Mar 2022 19:10:51 -0400 Subject: [PATCH 02/11] Added TagField for components --- .../base_systems/components/__init__.py | 2 +- .../base_systems/components/component.py | 5 +++ .../base_systems/components/dbfield.py | 43 +++++++++++++++++++ .../contrib/base_systems/components/holder.py | 14 ++++++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/base_systems/components/__init__.py b/evennia/contrib/base_systems/components/__init__.py index 705e9ee411..1aa94a1df1 100644 --- a/evennia/contrib/base_systems/components/__init__.py +++ b/evennia/contrib/base_systems/components/__init__.py @@ -9,7 +9,7 @@ See the docs for more information. """ from evennia.contrib.base_systems.components.component import Component -from evennia.contrib.base_systems.components.dbfield import DBField, NDBField +from evennia.contrib.base_systems.components.dbfield import DBField, NDBField, TagField from evennia.contrib.base_systems.components.holder import ComponentHolderMixin, ComponentProperty diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index f260610e54..4b1697da38 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -144,6 +144,11 @@ class Component: ndb_fields = getattr(self, "_ndb_fields", {}) return ndb_fields.keys() + @property + def tag_field_names(self): + tag_fields = getattr(self, "_tag_fields", {}) + return tag_fields.keys() + class ComponentRegisterError(Exception): pass diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 8e4f63b5da..c63ab1bebc 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -52,3 +52,46 @@ class NDBField(NAttributeProperty): ndb_fields = {} setattr(owner, '_ndb_fields', ndb_fields) ndb_fields[name] = self + + +class TagField: + """ + Component Descriptor to add a tag to the host. + """ + def __init__(self, default=None, enforce_single=False): + self._category_key = None + self._default = default + self._enforce_single = enforce_single + + def __set_name__(self, owner, name): + """ + Called when descriptor is first assigned to the class. It is called with + the name of the field. + + """ + self._category_key = f"{owner.name}__{name}" + tag_fields = getattr(owner, "_tag_fields", None) + if tag_fields is None: + tag_fields = {} + setattr(owner, '_tag_fields', tag_fields) + tag_fields[name] = self + + def __get__(self, instance, owner): + tag_value = instance.host.tags.get( + default=self._default, + category=self._category_key, + ) + return tag_value + + def __set__(self, instance, value): + tag_handler = instance.host.tags + if self._enforce_single: + tag_handler.clear(category=self._category_key) + + tag_handler.add( + key=self._key, + category=self._category_key, + ) + + def __delete__(self, instance): + instance.host.tags.clear(category=self._category_key) diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index ef0be1a242..651ae5840a 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -66,6 +66,10 @@ class ComponentHandler: self.db_names.append(component.name) self.host.tags.add(component.name, category="components") component.at_added(self.host) + for tag_field_name in component.tag_field_names: + default_tag = type(component).__dict__[tag_field_name]._default + if default_tag: + setattr(component, tag_field_name, default_tag) def add_default(self, name): """ @@ -87,6 +91,10 @@ class ComponentHandler: self.db_names.append(name) self.host.tags.add(name, category="components") new_component.at_added(self.host) + for tag_field_name in component.tag_field_names: + default_tag = type(component).__dict__[tag_field_name]._default + if default_tag: + setattr(component, tag_field_name, default_tag) def remove(self, component): """ @@ -103,6 +111,8 @@ class ComponentHandler: component.at_removed(self.host) self.db_names.remove(component_name) self.host.tags.remove(component_name, category="components") + for tag_field_name in component.tag_field_names: + self.host.tags.remove() del self._loaded_components[component_name] else: message = f"Cannot remove {component_name} from {self.host.name} as it is not registered." @@ -217,6 +227,10 @@ class ComponentHolderMixin(object): component = component_class.create(self, **values) component_names.append(component_name) self.components._loaded_components[component_name] = component + for tag_field_name in component.tag_field_names: + default_tag = type(component).__dict__[tag_field_name]._default + if default_tag: + setattr(component, tag_field_name, default_tag) self.db.component_names = component_names From 79be6a46897c120b86bfb690dc1cc62b071e0e25 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 22 Mar 2022 09:39:35 -0400 Subject: [PATCH 03/11] Added some docstring, cleaned to avoid duplication, added relevant tests --- .../base_systems/components/dbfield.py | 15 ++++-- .../contrib/base_systems/components/holder.py | 46 +++++++++++------- .../contrib/base_systems/components/tests.py | 47 ++++++++++++++++++- 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index c63ab1bebc..e653d8bc17 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -56,7 +56,13 @@ class NDBField(NAttributeProperty): class TagField: """ - Component Descriptor to add a tag to the host. + Component Tags Descriptor. + Allows you to set Tags related to a component on the class. + The tags are set with a prefixed category, so it can support + multiple tags or enforce a single one. + + Default value of a tag is added when the component is registered. + Tags are removed if the component itself is removed. """ def __init__(self, default=None, enforce_single=False): self._category_key = None @@ -65,9 +71,8 @@ class TagField: def __set_name__(self, owner, name): """ - Called when descriptor is first assigned to the class. It is called with - the name of the field. - + Called when descriptor is first assigned to the class. + It is called with the name of the field. """ self._category_key = f"{owner.name}__{name}" tag_fields = getattr(owner, "_tag_fields", None) @@ -89,7 +94,7 @@ class TagField: tag_handler.clear(category=self._category_key) tag_handler.add( - key=self._key, + key=value, category=self._category_key, ) diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index 651ae5840a..ddd606151d 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -64,12 +64,8 @@ class ComponentHandler: """ self._set_component(component) self.db_names.append(component.name) - self.host.tags.add(component.name, category="components") + self._add_component_tags(component) component.at_added(self.host) - for tag_field_name in component.tag_field_names: - default_tag = type(component).__dict__[tag_field_name]._default - if default_tag: - setattr(component, tag_field_name, default_tag) def add_default(self, name): """ @@ -89,8 +85,19 @@ class ComponentHandler: new_component = component.default_create(self.host) self._set_component(new_component) self.db_names.append(name) - self.host.tags.add(name, category="components") + self._add_component_tags(new_component) new_component.at_added(self.host) + + def _add_component_tags(self, component): + """ + Private method that adds the Tags set on a Component via TagFields + It will also add the name of the component so objects can be filtered + by the components the implement. + + Args: + component (object): The component instance that is added. + """ + self.host.tags.add(component.name, category="components") for tag_field_name in component.tag_field_names: default_tag = type(component).__dict__[tag_field_name]._default if default_tag: @@ -108,11 +115,9 @@ class ComponentHandler: """ component_name = component.name if component_name in self._loaded_components: + self._remove_component_tags(component) component.at_removed(self.host) self.db_names.remove(component_name) - self.host.tags.remove(component_name, category="components") - for tag_field_name in component.tag_field_names: - self.host.tags.remove() del self._loaded_components[component_name] else: message = f"Cannot remove {component_name} from {self.host.name} as it is not registered." @@ -133,11 +138,24 @@ class ComponentHandler: message = f"Cannot remove {name} from {self.host.name} as it is not registered." raise ComponentIsNotRegistered(message) + self._remove_component_tags(instance) instance.at_removed(self.host) self.db_names.remove(name) - self.host.tags.remove(name, category="components") + del self._loaded_components[name] + def _remove_component_tags(self, component): + """ + Private method that will remove the Tags set on a Component via TagFields + It will also remove the component name tag. + + Args: + component (object): The component instance that is removed. + """ + self.host.tags.remove(component.name, category="components") + for tag_field_name in component.tag_field_names: + delattr(component, tag_field_name) + def get(self, name): """ Method to retrieve a cached Component instance by its name. @@ -227,10 +245,6 @@ class ComponentHolderMixin(object): component = component_class.create(self, **values) component_names.append(component_name) self.components._loaded_components[component_name] = component - for tag_field_name in component.tag_field_names: - default_tag = type(component).__dict__[tag_field_name]._default - if default_tag: - setattr(component, tag_field_name, default_tag) self.db.component_names = component_names @@ -239,8 +253,8 @@ class ComponentHolderMixin(object): Method that add component related tags that were set using ComponentProperty. """ super().basetype_posthook_setup() - for component_name in self.db.component_names: - self.tags.add(component_name, category="components") + for component in self.components._loaded_components.values(): + self.components._add_component_tags(component) @property def components(self) -> ComponentHandler: diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index b9b18f28ba..aa81bdf818 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -1,4 +1,4 @@ -from evennia.contrib.base_systems.components import Component, DBField +from evennia.contrib.base_systems.components import Component, DBField, TagField from evennia.contrib.base_systems.components.holder import ComponentProperty, ComponentHolderMixin from evennia.objects.objects import DefaultCharacter from evennia.utils.test_resources import EvenniaTest @@ -14,12 +14,17 @@ class ComponentTestB(Component): name = "test_b" my_int = DBField(default=1) my_list = DBField(default=[]) + default_tag = TagField(default="initial_value") + single_tag = TagField(enforce_single=True) + multiple_tags = TagField() + default_single_tag = TagField(default="initial_value", enforce_single=True) class RuntimeComponentTestC(Component): name = "test_c" my_int = DBField(default=6) my_dict = DBField(default={}) + added_tag = TagField(default="added_value") class CharacterWithComponents(ComponentHolderMixin, DefaultCharacter): @@ -111,7 +116,11 @@ class TestComponents(EvenniaTest): def test_host_has_class_component_tags(self): assert self.char1.tags.has(key="test_a", category="components") assert self.char1.tags.has(key="test_b", category="components") + assert self.char1.tags.has(key="initial_value", category="test_b__default_tag") + assert self.char1.test_b.default_tag == "initial_value" assert not self.char1.tags.has(key="test_c", category="components") + assert not self.char1.tags.has(category="test_b__single_tag") + assert not self.char1.tags.has(category="test_b__multiple_tags") def test_host_has_added_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) @@ -119,12 +128,16 @@ class TestComponents(EvenniaTest): test_c = self.char1.components.get('test_c') assert self.char1.tags.has(key="test_c", category="components") + assert self.char1.tags.has(key="added_value", category="test_c__added_tag") + assert test_c.added_tag == "added_value" def test_host_has_added_default_component_tags(self): self.char1.components.add_default("test_c") test_c = self.char1.components.get("test_c") assert self.char1.tags.has(key="test_c", category="components") + assert self.char1.tags.has(key="added_value", category="test_c__added_tag") + assert test_c.added_tag == "added_value" def test_host_remove_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) @@ -134,6 +147,7 @@ class TestComponents(EvenniaTest): handler.remove(rct) assert not self.char1.tags.has(key="test_c", category="components") + assert not self.char1.tags.has(key="added_value", category="test_c__added_tag") def test_host_remove_by_name_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) @@ -142,4 +156,33 @@ class TestComponents(EvenniaTest): assert self.char1.tags.has(key="test_c", category="components") handler.remove_by_name("test_c") - assert not self.char1.tags.has(key="test_c", category="components") \ No newline at end of file + assert not self.char1.tags.has(key="test_c", category="components") + assert not self.char1.tags.has(key="added_value", category="test_c__added_tag") + + def test_component_tags_only_hold_one_value_when_enforce_single(self): + test_b = self.char1.components.get('test_b') + test_b.single_tag = "first_value" + test_b.single_tag = "second value" + + assert self.char1.tags.has(key="second value", category="test_b__single_tag") + assert test_b.single_tag == "second value" + assert not self.char1.tags.has(key="first_value", category="test_b__single_tag") + + def test_component_tags_default_value_is_overridden_when_enforce_single(self): + test_b = self.char1.components.get('test_b') + test_b.default_single_tag = "second value" + + assert self.char1.tags.has(key="second value", category="test_b__default_single_tag") + assert test_b.default_single_tag == "second value" + assert not self.char1.tags.has(key="first_value", category="test_b__default_single_tag") + + def test_component_tags_support_multiple_values_by_default(self): + test_b = self.char1.components.get('test_b') + test_b.multiple_tags = "first value" + test_b.multiple_tags = "second value" + test_b.multiple_tags = "third value" + + assert all(val in test_b.multiple_tags for val in ("first value", "second value", "third value")) + assert self.char1.tags.has(key="first value", category="test_b__multiple_tags") + assert self.char1.tags.has(key="second value", category="test_b__multiple_tags") + assert self.char1.tags.has(key="third value", category="test_b__multiple_tags") From 909e34528b229cc6b9fcca6717a1d35642e07348 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 22 Mar 2022 09:54:01 -0400 Subject: [PATCH 04/11] Updated the readme to include tag functionality --- .../contrib/base_systems/components/README.md | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/base_systems/components/README.md b/evennia/contrib/base_systems/components/README.md index 75489e3008..fc594aa036 100644 --- a/evennia/contrib/base_systems/components/README.md +++ b/evennia/contrib/base_systems/components/README.md @@ -50,7 +50,29 @@ class Health(Component): health = DBField(default=1) ``` -Note that default is optional and will default to None +Note that default is optional and will default to None. + +Adding a component to a host will also a similarly named tag with 'components' as category. +A Component named health will appear as key="health, category="components". +This allows you to retrieve objects with specific components by searching with the tag. + +It is also possible to add Component Tags the same way, using TagField. +TagField accepts a default value and can be used to store a single or multiple tags. +Default values are automatically added when the component is added. +Component Tags are cleared from the host if the component is removed. + +Example: +```python +from evennia.contrib.base_systems.components import Component, TagField + +class Health(Component): + resistances = TagField() + vulnerability = TagField(default="fire", enforce_single=True) +``` + +The 'resistances' field in this example can be set to multiple times and it will keep the added tags. +The 'vulnerability' field in this example will override the previous tag with the new one. + Each typeclass using the ComponentHolderMixin can declare its components From cc258e6c1bf86a68a37155a7f563cada076d82be Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 22 Mar 2022 10:25:54 -0400 Subject: [PATCH 05/11] Clarified some docstring --- .../contrib/base_systems/components/dbfield.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index e653d8bc17..1aec17050e 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -71,8 +71,8 @@ class TagField: def __set_name__(self, owner, name): """ - Called when descriptor is first assigned to the class. - It is called with the name of the field. + Called when TagField is first assigned to the class. + It is called with the component class and the name of the field. """ self._category_key = f"{owner.name}__{name}" tag_fields = getattr(owner, "_tag_fields", None) @@ -82,6 +82,10 @@ class TagField: tag_fields[name] = self def __get__(self, instance, owner): + """ + Called when retrieving the value of the TagField. + It is called with the component instance and the class. + """ tag_value = instance.host.tags.get( default=self._default, category=self._category_key, @@ -89,6 +93,11 @@ class TagField: return tag_value def __set__(self, instance, value): + """ + Called when setting a value on the TagField. + It is called with the component instance and the value. + """ + tag_handler = instance.host.tags if self._enforce_single: tag_handler.clear(category=self._category_key) @@ -99,4 +108,8 @@ class TagField: ) def __delete__(self, instance): + """ + Used when 'del' is called on the TagField. + It is called with the component instance. + """ instance.host.tags.clear(category=self._category_key) From 39d300458c81aa002e9863f3c52c07e117e94355 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sat, 19 Mar 2022 19:12:09 -0400 Subject: [PATCH 06/11] Consider prototypes loaded only when called on search --- evennia/prototypes/prototypes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 89422c1ff7..f9b6c564d3 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -528,8 +528,10 @@ def search_prototype( """ # This will load the prototypes the first time they are searched - if not _MODULE_PROTOTYPE_MODULES: + loaded = getattr(load_module_prototypes, '_LOADED', False) + if not loaded: load_module_prototypes() + setattr(load_module_prototypes, '_LOADED', True) # prototype keys are always in lowecase if key: From 738a107caff7078f0f6eedd13f45c41ee351c37a Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sun, 27 Mar 2022 10:56:26 -0400 Subject: [PATCH 07/11] Changed TagField delimiter from __ to :: --- .../base_systems/components/dbfield.py | 2 +- .../contrib/base_systems/components/tests.py | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 1aec17050e..7e2d16edee 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -74,7 +74,7 @@ class TagField: Called when TagField is first assigned to the class. It is called with the component class and the name of the field. """ - self._category_key = f"{owner.name}__{name}" + self._category_key = f"{owner.name}::{name}" tag_fields = getattr(owner, "_tag_fields", None) if tag_fields is None: tag_fields = {} diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index aa81bdf818..c374971e3f 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -116,11 +116,11 @@ class TestComponents(EvenniaTest): def test_host_has_class_component_tags(self): assert self.char1.tags.has(key="test_a", category="components") assert self.char1.tags.has(key="test_b", category="components") - assert self.char1.tags.has(key="initial_value", category="test_b__default_tag") + assert self.char1.tags.has(key="initial_value", category="test_b::default_tag") assert self.char1.test_b.default_tag == "initial_value" assert not self.char1.tags.has(key="test_c", category="components") - assert not self.char1.tags.has(category="test_b__single_tag") - assert not self.char1.tags.has(category="test_b__multiple_tags") + assert not self.char1.tags.has(category="test_b::single_tag") + assert not self.char1.tags.has(category="test_b::multiple_tags") def test_host_has_added_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) @@ -128,7 +128,7 @@ class TestComponents(EvenniaTest): test_c = self.char1.components.get('test_c') assert self.char1.tags.has(key="test_c", category="components") - assert self.char1.tags.has(key="added_value", category="test_c__added_tag") + assert self.char1.tags.has(key="added_value", category="test_c::added_tag") assert test_c.added_tag == "added_value" def test_host_has_added_default_component_tags(self): @@ -136,7 +136,7 @@ class TestComponents(EvenniaTest): test_c = self.char1.components.get("test_c") assert self.char1.tags.has(key="test_c", category="components") - assert self.char1.tags.has(key="added_value", category="test_c__added_tag") + assert self.char1.tags.has(key="added_value", category="test_c::added_tag") assert test_c.added_tag == "added_value" def test_host_remove_component_tags(self): @@ -147,7 +147,7 @@ class TestComponents(EvenniaTest): handler.remove(rct) assert not self.char1.tags.has(key="test_c", category="components") - assert not self.char1.tags.has(key="added_value", category="test_c__added_tag") + assert not self.char1.tags.has(key="added_value", category="test_c::added_tag") def test_host_remove_by_name_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) @@ -157,24 +157,24 @@ class TestComponents(EvenniaTest): handler.remove_by_name("test_c") assert not self.char1.tags.has(key="test_c", category="components") - assert not self.char1.tags.has(key="added_value", category="test_c__added_tag") + assert not self.char1.tags.has(key="added_value", category="test_c::added_tag") def test_component_tags_only_hold_one_value_when_enforce_single(self): test_b = self.char1.components.get('test_b') test_b.single_tag = "first_value" test_b.single_tag = "second value" - assert self.char1.tags.has(key="second value", category="test_b__single_tag") + assert self.char1.tags.has(key="second value", category="test_b::single_tag") assert test_b.single_tag == "second value" - assert not self.char1.tags.has(key="first_value", category="test_b__single_tag") + assert not self.char1.tags.has(key="first_value", category="test_b::single_tag") def test_component_tags_default_value_is_overridden_when_enforce_single(self): test_b = self.char1.components.get('test_b') test_b.default_single_tag = "second value" - assert self.char1.tags.has(key="second value", category="test_b__default_single_tag") + assert self.char1.tags.has(key="second value", category="test_b::default_single_tag") assert test_b.default_single_tag == "second value" - assert not self.char1.tags.has(key="first_value", category="test_b__default_single_tag") + assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag") def test_component_tags_support_multiple_values_by_default(self): test_b = self.char1.components.get('test_b') @@ -183,6 +183,6 @@ class TestComponents(EvenniaTest): test_b.multiple_tags = "third value" assert all(val in test_b.multiple_tags for val in ("first value", "second value", "third value")) - assert self.char1.tags.has(key="first value", category="test_b__multiple_tags") - assert self.char1.tags.has(key="second value", category="test_b__multiple_tags") - assert self.char1.tags.has(key="third value", category="test_b__multiple_tags") + assert self.char1.tags.has(key="first value", category="test_b::multiple_tags") + assert self.char1.tags.has(key="second value", category="test_b::multiple_tags") + assert self.char1.tags.has(key="third value", category="test_b::multiple_tags") From 5af84817b515a94d86a4dd3410a4d640ed48b723 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 27 Mar 2022 18:04:40 +0200 Subject: [PATCH 08/11] Update doc strings for DefaultChannel --- docs/source/Concepts/Internationalization.md | 2 +- evennia/comms/comms.py | 36 +++++++++++++++----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/source/Concepts/Internationalization.md b/docs/source/Concepts/Internationalization.md index 1db1753dd2..733d28dd86 100644 --- a/docs/source/Concepts/Internationalization.md +++ b/docs/source/Concepts/Internationalization.md @@ -17,7 +17,7 @@ updated after May 2021 will be missing some translations. +===============+======================+==============+ | es | Spanish | Aug 2019 | +---------------+----------------------+--------------+ -| fr | French | Nov 2018 | +| fr | French | Mar 2022 | +---------------+----------------------+--------------+ | it | Italian | Feb 2015 | +---------------+----------------------+--------------+ diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index 990cd2b5ee..31e36a2279 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -19,15 +19,33 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase): create different types of communication channels. Class-level variables: - - `send_to_online_only` (bool, default True) - if set, will only try to - send to subscribers that are actually active. This is a useful optimization. - - `log_file` (str, default `"channel_{channelname}.log"`). This is the - log file to which the channel history will be saved. The `{channelname}` tag - will be replaced by the key of the Channel. If an Attribute 'log_file' - is set, this will be used instead. If this is None and no Attribute is found, - no history will be saved. - - `channel_prefix_string` (str, default `"[{channelname} ]"`) - this is used - as a simple template to get the channel prefix with `.channel_prefix()`. + - `send_to_online_only` (bool, default True) - if set, will only try to + send to subscribers that are actually active. This is a useful optimization. + - `log_file` (str, default `"channel_{channelname}.log"`). This is the + log file to which the channel history will be saved. The `{channelname}` tag + will be replaced by the key of the Channel. If an Attribute 'log_file' + is set, this will be used instead. If this is None and no Attribute is found, + no history will be saved. + - `channel_prefix_string` (str, default `"[{channelname} ]"`) - this is used + as a simple template to get the channel prefix with `.channel_prefix()`. It is used + in front of every channel message; use `{channelmessage}` token to insert the + name of the current channel. Set to `None` if you want no prefix (or want to + handle it in a hook during message generation instead. + - `channel_msg_nick_pattern`(str, default `"{alias}\\s*?|{alias}\\s+?(?P.+?)") - + this is what used when a channel subscriber gets a channel nick assigned to this + channel. The nickhandler uses the pattern to pick out this channel's name from user + input. The `{alias}` token will get both the channel's key and any set/custom aliases + per subscriber. You need to allow for an `` regex group to catch any message + that should be send to the channel. You usually don't need to change this pattern + unless you are changing channel command-style entirely. + - `channel_msg_nick_replacement` (str, default `"channel {channelname} = $1"` - this + is used by the nickhandler to generate a replacement string once the nickhandler (using + the `channel_msg_nick_pattern`) identifies that the channel should be addressed + to send a message to it. The `` regex pattern match from `channel_msg_nick_pattern` + will end up at the `$1` position in the replacement. Together, this allows you do e.g. + 'public Hello' and have that become a mapping to `channel public = Hello`. By default, + the account-level `channel` command is used. If you were to rename that command you must + tweak the output to something like `yourchannelcommandname {channelname} = $1`. """ From 985976490677658d62fd95bc338e322451efa8c7 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 27 Mar 2022 18:35:21 +0200 Subject: [PATCH 09/11] Fix component docs --- docs/source/Contribs/Contribs-Overview.md | 11 +++++++++++ docs/source/api/evennia.contrib.base_systems.md | 1 + 2 files changed, 12 insertions(+) diff --git a/docs/source/Contribs/Contribs-Overview.md b/docs/source/Contribs/Contribs-Overview.md index a80f9d99cb..a8138acfd3 100644 --- a/docs/source/Contribs/Contribs-Overview.md +++ b/docs/source/Contribs/Contribs-Overview.md @@ -31,6 +31,7 @@ login systems, new command syntaxes, and build helpers._ Contrib-AWSStorage.md Contrib-Building-Menu.md Contrib-Color-Markups.md +Contrib-Components.md Contrib-Custom-Gametime.md Contrib-Email-Login.md Contrib-Ingame-Python.md @@ -79,6 +80,16 @@ Additional color markup styles for Evennia (extending or replacing the default +### Contrib: `components` + +__Contrib by ChrisLR 2021__ + +# The Components Contrib + +[Read the documentation](./Contrib-Components.md) - [Browse the Code](evennia.contrib.base_systems.components) + + + ### Contrib: `custom_gametime` _Contrib by vlgeoff, 2017 - based on Griatch's core original_ diff --git a/docs/source/api/evennia.contrib.base_systems.md b/docs/source/api/evennia.contrib.base_systems.md index 3de3dc1e4e..ccc1e8a0a1 100644 --- a/docs/source/api/evennia.contrib.base_systems.md +++ b/docs/source/api/evennia.contrib.base_systems.md @@ -14,6 +14,7 @@ evennia.contrib.base\_systems evennia.contrib.base_systems.awsstorage evennia.contrib.base_systems.building_menu evennia.contrib.base_systems.color_markups + evennia.contrib.base_systems.components evennia.contrib.base_systems.custom_gametime evennia.contrib.base_systems.email_login evennia.contrib.base_systems.ingame_python From 8f1f6047086d65415c966ee3739f82ac7547a6b4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 27 Mar 2022 18:56:37 +0200 Subject: [PATCH 10/11] Missing f-string marker in channel error msg --- evennia/commands/default/comms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index d1219c1a4e..b150bdaf84 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -286,7 +286,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): elif len(channels) > 1: self.msg( "Multiple possible channel matches/alias for " - "'{channelname}':\n" + ", ".join(chan.key for chan in channels) + f"'{channelname}':\n" + ", ".join(chan.key for chan in channels) ) return None return channels[0] From ef7280f55ae00d9df27ce51cef7b8e469417fdb2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 9 Apr 2022 15:39:39 +0200 Subject: [PATCH 11/11] Add TagProperty, AliasProperty, PermissionProperty. Default autocreate=True for AttributeProperty. --- .gitignore | 3 + CHANGELOG.md | 2 + docs/source/Components/Attributes.md | 186 ++++++++------------------ docs/source/Components/Permissions.md | 2 +- docs/source/Components/Tags.md | 37 +++-- evennia/accounts/accounts.py | 2 + evennia/comms/comms.py | 3 + evennia/objects/objects.py | 2 + evennia/objects/tests.py | 55 +++++++- evennia/scripts/scripts.py | 11 ++ evennia/typeclasses/attributes.py | 60 +++++++-- evennia/typeclasses/models.py | 12 ++ evennia/typeclasses/tags.py | 117 +++++++++++++++- evennia/typeclasses/tests.py | 2 +- 14 files changed, 334 insertions(+), 160 deletions(-) diff --git a/.gitignore b/.gitignore index 68662b37b9..a872de84a7 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ docs/build # Visual Studio Code (VS-Code) .vscode/ + +# Obsidian +.obsidian diff --git a/CHANGELOG.md b/CHANGELOG.md index 6268fa57ec..79e2870179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -160,6 +160,8 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10 - Attribute storage support defaultdics (Hendher) - Add ObjectParent mixin to default game folder template as an easy, ready-made way to override features on all ObjectDB-inheriting objects easily. +- Add `TagProperty`, `AliasProperty` and `PermissionProperty` to assign these + data in a similar way to django fields. ## Evennia 0.9.5 diff --git a/docs/source/Components/Attributes.md b/docs/source/Components/Attributes.md index d5eaa49842..18291ff774 100644 --- a/docs/source/Components/Attributes.md +++ b/docs/source/Components/Attributes.md @@ -5,21 +5,31 @@ > set obj/myattr = "test" ``` ```{code-block} python -:caption: In-code -obj.db.foo = [1,2,3, "bar"] +:caption: In-code, using the .db wrapper +obj.db.foo = [1, 2, 3, "bar"] value = obj.db.foo - +``` +```{code-block} python +:caption: In-code, using the .attributes handler obj.attributes.add("myattr", 1234, category="bar") value = attributes.get("myattr", category="bar") ``` +```{code-block} python +:caption: In-code, using `AttributeProperty` at class level +from evennia import DefaultObject +from evennia import AttributeProperty -_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a -server reboot. An Attribute can store pretty much any +class MyObject(DefaultObject): + foo = AttributeProperty(default=[1, 2, 3, "bar"]) + myattr = AttributeProperty(100, category='bar') + +``` + +_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a server reboot. An Attribute can store pretty much any Python data structure and data type, like numbers, strings, lists, dicts etc. You can also store (references to) database objects like characters and rooms. -- [What can be stored in an Attribute](#what-types-of-data-can-i-save-in-an-attribute) is a must-read - also for experienced developers, to avoid getting surprised. Attributes can store _almost_ everything +- [What can be stored in an Attribute](#what-types-of-data-can-i-save-in-an-attribute) is a must-read to avoid being surprised, also for experienced developers. Attributes can store _almost_ everything but you need to know the quirks. - [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent siblings of Attributes. @@ -29,7 +39,7 @@ store (references to) database objects like characters and rooms. Attributes are usually handled in code. All [Typeclassed](./Typeclasses.md) entities ([Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and -[Channels](./Channels.md)) all can (and usually do) have Attributes associated with them. There +[Channels](./Channels.md)) can (and usually do) have Attributes associated with them. There are three ways to manage Attributes, all of which can be mixed. - [Using the `.db` property shortcut](#using-db) @@ -38,7 +48,7 @@ are three ways to manage Attributes, all of which can be mixed. ### Using .db -The simplest way to get/set Attributes is to use the `.db` shortcut: +The simplest way to get/set Attributes is to use the `.db` shortcut. This allows for setting and getting Attributes that lack a _category_ (having category `None`) ```python import evennia @@ -78,9 +88,8 @@ default `all` functionality until you delete it again. ### Using .attributes -If you don't know the name of the Attribute beforehand you can also use -the `AttributeHandler`, available as `.attributes`. With no extra keywords this is identical -to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally): +If you want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of +the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally): ```python is_ouch = rose.attributes.get("has_thorns") @@ -92,8 +101,7 @@ helmet = obj.attributes.get("helmet") obj.attributes.add("my game log", "long text about ...") ``` -With the `AttributeHandler` you can also give Attributes a `category`. By using a category you can -separate same-named Attributes on the same object which can help organization: +By using a category you can separate same-named Attributes on the same object to help organization. ```python # store (let's say we have gold_necklace and ringmail_armor from before) @@ -105,11 +113,7 @@ neck_clothing = obj.attributes.get("neck", category="clothing") neck_armor = obj.attributes.get("neck", category="armor") ``` -If you don't specify a category, the Attribute's `category` will be `None`. Note that -`None` is also considered a category of its own, so you won't find `None`-category Attributes mixed -with Attributes having categories. - -> When using `.db`, you will always use the `None` category. +If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories. Here are the methods of the `AttributeHandler`. See the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for more details. @@ -151,9 +155,8 @@ all_clothes = obj.attributes.all(category="clothes") ### Using AttributeProperty -There is a third way to set up an Attribute, and that is by setting up an `AttributeProperty`. This -is done on the _class level_ of your typeclass and allows you to treat Attributes a bit like Django -database Fields. +The third way to set up an Attribute is to use an `AttributeProperty`. This +is done on the _class level_ of your typeclass and allows you to treat Attributes a bit like Django database Fields. Unlike using `.db` and `.attributes`, an `AttributeProperty` can't be created on the fly, you must assign it in the class code. ```python # mygame/typeclasses/characters.py @@ -163,133 +166,62 @@ from evennia.typeclasses.attributes import AttributeProperty class Character(DefaultCharacter): - strength = AttributeProperty(default=10, category='stat', autocreate=True) - constitution = AttributeProperty(default=10, category='stat', autocreate=True) - agility = AttributeProperty(default=10, category='stat', autocreate=True) - magic = AttributeProperty(default=10, category='stat', autocreate=True) - - sleepy = AttributeProperty(default=False) - poisoned = AttributeProperty(default=False) + strength = AttributeProperty(10, category='stat') + constitution = AttributeProperty(11, category='stat') + agility = AttributeProperty(12, category='stat') + magic = AttributeProperty(13, category='stat') + + sleepy = AttributeProperty(False, autocreate=False) + poisoned = AttributeProperty(False, autocreate=False) def at_object_creation(self): # ... ``` -These "Attribute-properties" will be made available to all instances of the class. +When a new instance of the class is created, new `Attributes` will be created with the value and category given. -```{important} -If you change the `default` of an `AttributeProperty` (and reload), it will -change the default for _all_ instances of that class (it will not override -explicitly changed values). -``` - -```python -char = evennia.search_object(Character, key="Bob")[0] # returns list, get 0th element - -# get defaults -strength = char.strength # will get the default value 10 - -# assign new values (this will create/update new Attributes) -char.strength = 12 -char.constitution = 16 -char.agility = 8 -char.magic = 2 - -# you can also do arithmetic etc -char.magic += 2 # char.magic is now 4 - -# check Attributes -strength = char.strength # this is now 12 -is_sleepy = char.sleepy -is_poisoned = char.poisoned - -del char.strength # wipes the Attribute -strength = char.strengh # back to the default (10) again -``` - -See the [AttributeProperty](evennia.typeclasses.attributes.AttributeProperty) docs for more -details on arguments. - -An `AttributeProperty` will _not_ create an `Attribute` by default. A new `Attribute` will be created -(or an existing one retrieved/updated) will happen differently depending on how the `autocreate` -keyword: - -- If `autocreate=False` (default), an `Attribute` will be created only if the field is explicitly - assigned a value (even if the value is the same as the default, such as `char.strength = 10`). -- If `autocreate=True`, an `Attribute` will be created as soon as the field is _accessed_ in - any way (So both `strength = char.strength` and `char.strength = 10` will both make sure that - an `Attribute` exists. - -Example: +With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object: ```python -# in mygame/typeclasses/objects.py +char = create_object(Character) -from evennia import create_object -from evennia import DefaultObject -from evennia.typeclasses.attributes import AttributeProperty +char.strength # returns 10 +char.agility = 15 # assign a new value (category remains 'stat') -class Object(DefaultObject): - - value_a = AttributeProperty(default="foo") - value_b = AttributeProperty(default="bar", autocreate=True) - -obj = evennia.create_object(key="Dummy") +char.db.magic # returns None (wrong category) +char.attributes.get("agility", category="stat") # returns 15 -# these will find NO Attributes! -obj.db.value_a -obj.attributes.get("value_a") -obj.db.value_b -obj.attributes.get("value_b") +char.db.sleepy # returns None because autocreate=False (see below) -# get data from attribute-properties -vala = obj.value_a # returns "foo" -valb = obj.value_b # return "bar" AND creates the Attribute (autocreate) - -# the autocreate property will now be found -obj.db.value_a # still not found -obj.attributes.get("value_a") # '' -obj.db.value_b # now returns "bar" -obj.attributes.get("value_b") # '' - -# assign new values -obj.value_a = 10 # will now create a new Attribute -obj.value_b = 12 # will update the existing Attribute - -# both are now found as Attributes -obj.db.value_a # now returns 10 -obj.attributes.get("value_a") # '' -obj.db.value_b # now returns 12 -obj.attributes.get("value_b") # '' ``` -If you always access your Attributes via the `AttributeProperty` this does not matter that much -(it's also a bit of an optimization to not create an actual database `Attribute` unless the value changed). -But until an `Attribute` has been created, `AttributeProperty` fields will _not_ show up with the -`examine` command or by using the `.db` or `.attributes` handlers - so this is a bit inconsistent. -If this is important, you need to 'initialize' them by accessing them at least once ... something -like this: +```{warning} +Be careful to not assign AttributeProperty's to names of properties and methods already existing on the class, like 'key' or 'at_object_creation'. That could lead to very confusing errors. +``` +The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set. +The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default. +The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database): ```python -# ... -class Character(DefaultCharacter): +char.sleepy # returns False, no db access - strength = AttributeProperty(12, autocreate=True) - agility = AttributeProperty(12, autocreate=True) +char.db.sleepy # returns None - no Attribute exists +char.attributes.get("sleepy") # returns None too +char.sleepy = True # now an Attribute is created +char.db.sleepy # now returns True! +char.attributes.get("sleepy") # now returns True + +char.sleepy # now returns True, involves db access - def at_object_creation(self): - # initializing - self.strength # by accessing it, the Attribute is auto-created - self.agility # '' ``` -```{important} -If you created your `AttributeProperty` with a `category`, you *must* specify the -category in `.attributes.get()` if you want to find it this way. Remember that -`.db` always uses a `category` of `None`. -``` +You can e.g. `del char.strength` to set the value back to the default (the value defined +in the `AttributeProperty`). + +See the [AttributeProperty API](evennia.typeclasses.attributes.AttributeProperty) for more details on how to create it with special options, like giving access-restrictions. + ## Managing Attributes in-game diff --git a/docs/source/Components/Permissions.md b/docs/source/Components/Permissions.md index cac5e05b0f..84d4028a77 100644 --- a/docs/source/Components/Permissions.md +++ b/docs/source/Components/Permissions.md @@ -17,7 +17,7 @@ All new accounts are given a default set of permissions defined by ## Managing Permissions In-game, you use the `perm` command to add and remove permissions - +j perm/account Tommy = Builders perm/account/del Tommy = Builders diff --git a/docs/source/Components/Tags.md b/docs/source/Components/Tags.md index 2234c1c14b..488bab577f 100644 --- a/docs/source/Components/Tags.md +++ b/docs/source/Components/Tags.md @@ -1,20 +1,33 @@ # Tags +```{code-block} +:caption: In game +> tag obj = tagname +``` +```{code-block} python +:caption: In code, using .tags (TagHandler) -A common task of a game designer is to organize and find groups of objects and do operations on -them. A classic example is to have a weather script affect all "outside" rooms. Another would be for -a player casting a magic spell that affects every location "in the dungeon", but not those -"outside". Another would be to quickly find everyone joined with a particular guild or everyone -currently dead. +obj.tags.add("mytag", category="foo") +obj.tags.get("mytag", category="foo") +``` -*Tags* are short text labels that you attach to objects so as to easily be able to retrieve and -group them. An Evennia entity can be tagged with any number of Tags. On the database side, Tag -entities are *shared* between all objects with that tag. This makes them very efficient but also -fundamentally different from [Attributes](./Attributes.md), each of which always belongs to one *single* -object. +```{code-block} python +:caption: In code, using TagProperty (auto-assign tag to all instances of the class) -In Evennia, Tags are technically also used to implement `Aliases` (alternative names for objects) -and `Permissions` (simple strings for [Locks](./Locks.md) to check for). +from evennia import DefaultObject +from evennia import TagProperty +class Sword(DefaultObject): + can_be_wielded = TagProperty(category='combat') + has_sharp_edge = TagProperty(category='combat') + +``` + +_Tags_ are short text lables one can 'hang' on objects in order to organize, group and quickly find out their properties. An Evennia entity can be tagged by any number of tags. They are more efficient than [Attributes](Attributes) since on the database-side, Tags are _shared_ between all objects with that particular tag. A tag does not carry a value in itself; it either sits on the entity + +Above, the tags inform us that the `Sword` is both sharp and can be wielded. If that's all they do, they could just be a normal Python flag. When tags become important is if there are a lot of objects with different combinations of tags. Maybe you have a magical spell that dulls _all_ sharp-edged objects in the castle - whether sword, dagger, spear or kitchen knife! You can then just grab all objects with the `has_sharp_edge` tag. +Another example would be a weather script affecting all rooms tagged as `outdoors` or finding all characters tagged with `belongs_to_fighter_guild`. + +In Evennia, Tags are technically also used to implement `Aliases` (alternative names for objects) and `Permissions` (simple strings for [Locks](./Locks.md) to check for). ## Properties of Tags (and Aliases and Permissions) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index ed1443de09..f779a4281f 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -1253,6 +1253,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ self.basetype_setup() self.at_account_creation() + # initialize Attribute/TagProperties + self.init_evennia_properties() permissions = [settings.PERMISSION_ACCOUNT_DEFAULT] if hasattr(self, "_createdict"): diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index 31e36a2279..c8fd53e4b5 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -76,6 +76,9 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase): """ self.basetype_setup() self.at_channel_creation() + # initialize Attribute/TagProperties + self.init_evennia_properties() + if hasattr(self, "_createdict"): # this is only set if the channel was created # with the utils.create.create_channel function. diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index cd629bd22a..d2a2b6e8a4 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1228,6 +1228,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ self.basetype_setup() self.at_object_creation() + # initialize Attribute/TagProperties + self.init_evennia_properties() if hasattr(self, "_createdict"): # this will only be set if the utils.create function diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index 33a666b1a8..6fe443cab8 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -1,6 +1,9 @@ -from evennia.utils.test_resources import BaseEvenniaTest +from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit +from evennia.typeclasses.attributes import AttributeProperty +from evennia.typeclasses.tags import TagProperty, AliasProperty, PermissionProperty from evennia.objects.models import ObjectDB +from evennia.objects.objects import DefaultObject from evennia.utils import create @@ -227,3 +230,53 @@ class TestContentHandler(BaseEvenniaTest): self.obj2.move_to(self.room1) self.obj2.move_to(self.room2) self.assertEqual(self.room2.contents, [self.obj1, self.obj2]) + + +class TestObjectPropertiesClass(DefaultObject): + attr1 = AttributeProperty(default="attr1") + attr2 = AttributeProperty(default="attr2", category="attrcategory") + attr3 = AttributeProperty(default="attr3", autocreate=False) + tag1 = TagProperty() + tag2 = TagProperty(category="tagcategory") + testalias = AliasProperty() + testperm = PermissionProperty() + +class TestProperties(EvenniaTestCase): + """ + Test Properties. + + """ + def setUp(self): + self.obj = create.create_object(TestObjectPropertiesClass, key="testobj") + + def tearDown(self): + self.obj.delete() + + def test_properties(self): + """ + Test all properties assigned at class level. + """ + obj = self.obj + + self.assertEqual(obj.db.attr1, "attr1") + self.assertEqual(obj.attributes.get("attr1"), "attr1") + self.assertEqual(obj.attr1, "attr1") + + self.assertEqual(obj.attributes.get("attr2", category="attrcategory"), "attr2") + self.assertEqual(obj.db.attr2, None) # category mismatch + self.assertEqual(obj.attr2, "attr2") + + self.assertEqual(obj.db.attr3, None) # non-autocreate, so not in db yet + self.assertFalse(obj.attributes.has("attr3")) + self.assertEqual(obj.attr3, "attr3") + + obj.attr3 = "attr3b" # stores it in db! + + self.assertEqual(obj.db.attr3, "attr3b") + self.assertTrue(obj.attributes.has("attr3")) + + self.assertTrue(obj.tags.has("tag1")) + self.assertTrue(obj.tags.has("tag2", category="tagcategory")) + + self.assertTrue(obj.aliases.has("testalias")) + self.assertTrue(obj.permissions.has("testperm")) diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index d24ddd4a3c..aac89ce719 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -400,7 +400,10 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase): overriding the call (unused by default). """ + self.basetype_setup() self.at_script_creation() + # initialize Attribute/TagProperties + self.init_evennia_properties() if hasattr(self, "_createdict"): # this will only be set if the utils.create_script @@ -471,6 +474,14 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase): super().delete() return True + def basetype_setup(self): + """ + Changes fundamental aspects of the type. Usually changes are made in at_script creation + instead. + + """ + pass + def at_init(self): """ Called when the Script is cached in the idmapper. This is usually more reliable diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index a43f8382e4..7c5a537c19 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -176,7 +176,7 @@ class AttributeProperty: attrhandler_name = "attributes" - def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=False): + def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=True): """ Initialize an Attribute as a property descriptor. @@ -188,12 +188,12 @@ class AttributeProperty: lockstring (str): This is not itself useful with the property, but only if using the full AttributeHandler.get(accessing_obj=...) to access the Attribute. - autocreate (bool): If an un-found Attr should lead to auto-creating the - Attribute (with the default value). If `False`, the property will - return the default value until it has been explicitly set. This means - less database accesses, but also means the property will have no - corresponding Attribute if wanting to access it directly via the - AttributeHandler (it will also not show up in `examine`). + autocreate (bool): True by default; this means Evennia makes sure to create a new + copy of the Attribute (with the default value) whenever a new object with this + property is created. If `False`, no Attribute will be created until the property + is explicitly assigned a value. This makes it more efficient while it retains + its default (there's no db access), but without an actual Attribute generated, + one cannot access it via .db, the AttributeHandler or see it with `examine`. """ self._default = default @@ -218,21 +218,20 @@ class AttributeProperty: """ value = self._default try: - value = getattr(instance, self.attrhandler_name).get( + value = self.at_get(getattr(instance, self.attrhandler_name).get( key=self._key, default=self._default, category=self._category, strattr=self._strattr, raise_exception=self._autocreate, - ) + )) except AttributeError: if self._autocreate: # attribute didn't exist and autocreate is set self.__set__(instance, self._default) else: raise - finally: - return value + return value def __set__(self, instance, value): """ @@ -242,7 +241,7 @@ class AttributeProperty: ( getattr(instance, self.attrhandler_name).add( self._key, - value, + self.at_set(value), category=self._category, lockstring=self._lockstring, strattr=self._strattr, @@ -251,10 +250,43 @@ class AttributeProperty: def __delete__(self, instance): """ - Called when running `del` on the field. Will remove/clear the Attribute. + Called when running `del` on the property. Will remove/clear the Attribute. Note that + the Attribute will be recreated next retrieval unless the AttributeProperty is also + removed in code! """ - (getattr(instance, self.attrhandler_name).remove(key=self._key, category=self._category)) + getattr(instance, self.attrhandler_name).remove(key=self._key, category=self._category) + + def at_set(self, value): + """ + The value to set is passed through the method. It can be used to customize/validate + the input in a custom child class. + + Args: + value (any): The value about to the stored in this Attribute. + + Returns: + any: The value to store. + + Raises: + AttributeError: If the value is invalid to store. + + """ + return value + + def at_get(self, value): + """ + The value returned from the Attribute is passed through this method. It can be used + to react to the retrieval or modify the result in some way. + + Args: + value (any): Value returned from the Attribute. + + Returns: + any: The value to return to the caller. + + """ + return value class NAttributeProperty(AttributeProperty): diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index 23acf4e714..2a71835e0c 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -325,6 +325,18 @@ class TypedObject(SharedMemoryModel): super().__init__(*args, **kwargs) self.set_class_from_typeclass(typeclass_path=typeclass_path) + def init_evennia_properties(self): + """ + Called by creation methods; makes sure to initialize Attribute/TagProperties + by fetching them once. + """ + for propkey, prop in self.__class__.__dict__.items(): + if hasattr(prop, "__set_name__"): + try: + getattr(self, propkey) + except Exception: + log_trace() + # initialize all handlers in a lazy fashion @lazy_property def attributes(self): diff --git a/evennia/typeclasses/tags.py b/evennia/typeclasses/tags.py index 1dffb0cfb8..7e0926156b 100644 --- a/evennia/typeclasses/tags.py +++ b/evennia/typeclasses/tags.py @@ -96,6 +96,75 @@ class Tag(models.Model): # Handlers making use of the Tags model # +class TagProperty: + """ + Tag property descriptor. Allows for setting tags on an object as Django-like 'fields' + on the class level. Since Tags are almost always used for querying, Tags are always + created/assigned along with the object. Make sure the property/tagname does not collide + with an existing method/property on the class. If it does, you must use tags.add() + instead. + + Example: + :: + + class Character(DefaultCharacter): + mytag = TagProperty() # category=None + mytag2 = TagProperty(category="tagcategory") + + """ + taghandler_name = "tags" + + def __init__(self, category=None, data=None): + self._category = category + self._data = data + self._key = "" + + def __set_name__(self, cls, name): + """ + Called when descriptor is first assigned to the class (not the instance!). + It is called with the name of the field. + + """ + self._key = name + + def __get__(self, instance, owner): + """ + Called when accessing the tag as a property on the instance. + + """ + try: + return getattr(instance, self.taghandler_name).get( + key=self._key, + category=self._category, + return_list=False, + raise_exception=True + ) + except AttributeError: + self.__set__(instance, self._category) + + def __set__(self, instance, category): + """ + Assign a new category to the tag. It's not possible to set 'data' this way. + + """ + self._category = category + ( + getattr(instance, self.taghandler_name).add( + key=self._key, + category=self._category, + data=self._data + ) + ) + + def __delete__(self, instance): + """ + Called when running `del` on the property. Will disconnect the object from + the Tag. Note that the tag will be readded on next fetch unless the + TagProperty is also removed in code! + + """ + getattr(instance, self.taghandler_name).remove(key=self._key, category=self._category) + class TagHandler(object): """ @@ -361,7 +430,8 @@ class TagHandler(object): return ret[0] if len(ret) == 1 else ret - def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False): + def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False, + raise_exception=False): """ Get the tag for the given key, category or combination of the two. @@ -376,6 +446,8 @@ class TagHandler(object): instead of a string representation of the Tag. return_list (bool, optional): Always return a list, regardless of number of matches. + raise_exception (bool, optional): Raise AttributeError if no matches + are found. Returns: tags (list): The matches, either string @@ -383,6 +455,9 @@ class TagHandler(object): depending on `return_tagobj`. If 'default' is set, this will be a list with the default value as its only element. + Raises: + AttributeError: If finding no matches and `raise_exception` is True. + """ ret = [] for keystr in make_iter(key): @@ -393,9 +468,14 @@ class TagHandler(object): for tag in self._getcache(keystr, category) ] ) - if return_list: - return ret if ret else [default] if default is not None else [] - return ret[0] if len(ret) == 1 else (ret if ret else default) + if not ret: + if raise_exception: + raise AttributeError(f"No tags found matching input {key}, {category}.") + elif return_list: + return [default] if default is not None else [] + else: + return default + return ret if return_list else (ret[0] if len(ret) == 1 else ret) def remove(self, key=None, category=None): """ @@ -521,6 +601,21 @@ class TagHandler(object): return ",".join(self.all()) +class AliasProperty(TagProperty): + """ + Allows for setting aliases like Django fields: + :: + + class Character(DefaultCharacter): + # note that every character will get the alias bob. Make sure + # the alias property does not collide with an existing method + # or property on the class. + bob = AliasProperty() + + """ + taghandler_name = "aliases" + + class AliasHandler(TagHandler): """ A handler for the Alias Tag type. @@ -530,6 +625,20 @@ class AliasHandler(TagHandler): _tagtype = "alias" +class PermissionProperty(TagProperty): + """ + Allows for setting permissions like Django fields: + :: + + class Character(DefaultCharacter): + # note that every character will get this permission! Make + # sure it doesn't collide with an existing method or property. + myperm = PermissionProperty() + + """ + taghandler_name = "permissions" + + class PermissionHandler(TagHandler): """ A handler for the Permission Tag type. diff --git a/evennia/typeclasses/tests.py b/evennia/typeclasses/tests.py index aef536dbe0..aea6ce7518 100644 --- a/evennia/typeclasses/tests.py +++ b/evennia/typeclasses/tests.py @@ -3,7 +3,7 @@ Unit tests for typeclass base system """ from django.test import override_settings -from evennia.utils.test_resources import BaseEvenniaTest +from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase from evennia.typeclasses import attributes from mock import patch from parameterized import parameterized