diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 656e8869..b79791ac 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,12 @@ Changelog
Due to this library relying on external content, older versions are not guaranteed to work.
Try to always use the latest version.
+.. _v2.4.0:
+2.4.0 (2019-11-20)
+==================
+- Added support for multiple houses per character. Accessible on ``Character.houses`` field.
+- ``Character.house`` is now deprecated. It will contain the character's first house or ``None``.
+
.. _v2.3.4:
2.3.4 (2019-11-14)
==================
diff --git a/tests/resources/README.md b/tests/resources/README.md
index 0e6b9c77..74c8e102 100644
--- a/tests/resources/README.md
+++ b/tests/resources/README.md
@@ -11,6 +11,7 @@ deaths by summons, players, assisted deaths, etcetera.
- [tibiacom_not_found.txt](character/tibiacom_not_found.txt) - A character not found page.
- [tibiacom_title_badges.txt](character/tibiacom_title_badges.txt) - A character with unlocked titles and badges.
- [tibiacom_no_badges_selected.txt](character/tibiacom_no_badges_selected.txt) - A character with no selected badges.
+- [tibiacom_multiple_houses.txt](character/tibiacom_multiple_houses.txt) - A character with two houses.
- [tibiadata.json](character/tibiadata.json) - A character on TibiaData, also showing Pvp deaths with assists.
- [tibiadata_deleted.json](character/tibiadata_deleted.json) - A character scheduled for deletion on TibiaData.
- [tibiadata_not_found.json](character/tibiadata_not_found.json) - The response of a character not found on TibiaData.
diff --git a/tests/resources/character/tibiacom_multiple_houses.txt b/tests/resources/character/tibiacom_multiple_houses.txt
new file mode 100644
index 00000000..0bedf8f4
--- /dev/null
+++ b/tests/resources/character/tibiacom_multiple_houses.txt
@@ -0,0 +1,87 @@
+
+
+
\ No newline at end of file
diff --git a/tests/tests_character.py b/tests/tests_character.py
index 8bd541c0..6b93b4f9 100644
--- a/tests/tests_character.py
+++ b/tests/tests_character.py
@@ -15,6 +15,7 @@
FILE_CHARACTER_DEATHS_COMPLEX = "character/tibiacom_deaths_complex.txt"
FILE_CHARACTER_TITLE_BADGES = "character/tibiacom_title_badges.txt"
FILE_CHARACTER_NO_BADGES_SELECTED = "character/tibiacom_no_badges_selected.txt"
+FILE_CHARACTER_MULTIPLE_HOUSES = "character/tibiacom_multiple_houses.txt"
FILE_CHARACTER_TIBIADATA = "character/tibiadata.json"
FILE_CHARACTER_TIBIADATA_UNHIDDEN = "character/tibiadata_unhidden.json"
@@ -128,6 +129,20 @@ def test_character_from_content_no_selected_badges(self):
self.assertEqual(0, len(char.account_badges))
self.assertEqual(0, len(char.former_names))
+ def test_character_from_content_multiple_houses(self):
+ """Testing parsing a character with multiple houses."""
+ content = self._load_resource(FILE_CHARACTER_MULTIPLE_HOUSES)
+ char = Character.from_content(content)
+ self.assertEqual("Sayuri Nowan", char.name)
+ self.assertEqual(2, len(char.houses))
+ self.assertEqual(char.house.name, char.houses[0].name)
+ first_house = char.houses[0]
+ second_house = char.houses[1]
+ self.assertEqual("Cormaya 10", first_house.name)
+ self.assertEqual("Old Heritage Estate", second_house.name)
+ self.assertEqual("Edron", first_house.town)
+ self.assertEqual("Rathleton", second_house.town)
+
def test_character_from_content_unrelated(self):
"""Testing parsing an unrelated tibia.com section"""
content = self._load_resource(self.FILE_UNRELATED_SECTION)
diff --git a/tibiapy/__init__.py b/tibiapy/__init__.py
index a6427044..e8ff04f4 100644
--- a/tibiapy/__init__.py
+++ b/tibiapy/__init__.py
@@ -13,7 +13,7 @@
from tibiapy.creature import *
from tibiapy.client import *
-__version__ = '2.3.4'
+__version__ = '2.4.0'
from logging import NullHandler
diff --git a/tibiapy/character.py b/tibiapy/character.py
index 14cd0cf0..1e94f17e 100644
--- a/tibiapy/character.py
+++ b/tibiapy/character.py
@@ -1,6 +1,7 @@
import datetime
import re
import urllib.parse
+import warnings
from collections import OrderedDict
from typing import List, Optional
@@ -12,7 +13,7 @@
from tibiapy.guild import Guild
from tibiapy.house import CharacterHouse
from tibiapy.utils import parse_json, parse_tibia_date, parse_tibia_datetime, parse_tibiacom_content, \
- parse_tibiadata_date, parse_tibiadata_datetime, try_datetime, try_enum
+ parse_tibiadata_date, parse_tibiadata_datetime, try_datetime, try_enum, deprecated
# Extracts the scheduled deletion date of a character."""
deleted_regexp = re.compile(r'([^,]+), will be deleted at (.*)')
@@ -159,8 +160,8 @@ class Character(abc.BaseCharacter):
The current hometown of the character.
married_to: :class:`str`, optional
The name of the character's spouse.
- house: :class:`CharacterHouse`, optional
- The house currently owned by the character.
+ houses: :class:`list` of :class:`CharacterHouse`
+ The houses currently owned by the character.
guild_membership: :class:`GuildMembership`, optional
The guild the character is a member of.
last_login: :class:`datetime.datetime`, optional
@@ -195,7 +196,7 @@ class Character(abc.BaseCharacter):
"former_world",
"residence",
"married_to",
- "house",
+ "houses",
"guild_membership",
"last_login",
"account_status",
@@ -222,7 +223,7 @@ def __init__(self, name=None, world=None, vocation=None, level=0, sex=None, **kw
self.former_world = kwargs.get("former_world") # type: Optional[str]
self.residence = kwargs.get("residence") # type: str
self.married_to = kwargs.get("married_to") # type: Optional[str]
- self.house = kwargs.get("house") # type: Optional[CharacterHouse]
+ self.houses = kwargs.get("house", []) # type: List[CharacterHouse]
self.guild_membership = kwargs.get("guild_membership") # type: Optional[GuildMembership]
self.last_login = try_datetime(kwargs.get("last_login"))
self.account_status = try_enum(AccountStatus, kwargs.get("account_status"))
@@ -265,6 +266,18 @@ def hidden(self):
def married_to_url(self):
""":class:`str`: The URL to the husband/spouse information page on Tibia.com, if applicable."""
return self.get_url(self.married_to) if self.married_to else None
+
+ @property
+ @deprecated("houses")
+ def house(self):
+ """:class:`CharacterHouse`: The house currently owned by the character.
+
+ .. deprecated:: 2.4.0
+ Characters may have more than one house. This will only return the first if any. Use :attr:`houses` instead.
+
+ Only kept for backwards compatibility and may be removed in the next major update.
+ """
+ return self.houses[0] if self.houses else None
# endregion
# region Public methods
@@ -350,8 +363,8 @@ def from_tibiadata(cls, content):
if "house" in character_data:
house = character_data["house"]
paid_until_date = parse_tibiadata_date(house["paid"])
- char.house = CharacterHouse(house["houseid"], house["name"], char.world, house["town"], char.name,
- paid_until_date)
+ char.houses.append(CharacterHouse(house["houseid"], house["name"], char.world, house["town"], char.name,
+ paid_until_date))
char.comment = character_data.get("comment")
if len(character_data["last_login"]) > 0:
char.last_login = parse_tibiadata_datetime(character_data["last_login"][0])
@@ -460,7 +473,7 @@ def _parse_character_information(self, rows):
"""
int_rows = ["level", "achievement_points"]
char = {}
- house = {}
+ houses = []
for row in rows:
cols_raw = row.find_all('td')
cols = [ele.text.strip() for ele in cols_raw]
@@ -475,8 +488,8 @@ def _parse_character_information(self, rows):
house_link = cols_raw[1].find('a')
url = urllib.parse.urlparse(house_link["href"])
query = urllib.parse.parse_qs(url.query)
- house = {"id": int(query["houseid"][0]), "name": house_link.text.strip(),
- "town": query["town"][0], "paid_until": paid_until_date}
+ houses.append({"id": int(query["houseid"][0]), "name": house_link.text.strip(),
+ "town": query["town"][0], "paid_until": paid_until_date})
continue
if field in int_rows:
value = int(value)
@@ -520,9 +533,8 @@ def _parse_character_information(self, rows):
# This means that there is a attribute in the character's information table that does not have a
# corresponding class attribute.
pass
- if house:
- self.house = CharacterHouse(house["id"], house["name"], self.world, house["town"], self.name,
- house["paid_until"])
+ self.houses = [CharacterHouse(h["id"], h["name"], self.world, h["town"], self.name, h["paid_until"])
+ for h in houses]
def _parse_deaths(self, rows):
"""
diff --git a/tibiapy/utils.py b/tibiapy/utils.py
index 4198f93a..488dad94 100644
--- a/tibiapy/utils.py
+++ b/tibiapy/utils.py
@@ -1,6 +1,8 @@
import datetime
+import functools
import json
import re
+import warnings
from typing import Optional, Type, TypeVar, Union
import bs4
@@ -394,3 +396,20 @@ def _recursive_strip(value):
if isinstance(value, str):
return value.strip()
return value
+
+
+def deprecated(instead=None):
+ def actual_decorator(func):
+ @functools.wraps(func)
+ def decorated(*args, **kwargs):
+ warnings.simplefilter('always', DeprecationWarning)
+ if instead:
+ fmt = "{0.__name__} is deprecated, use {1} instead."
+ else:
+ fmt = '{0.__name__} is deprecated.'
+
+ warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning)
+ warnings.simplefilter('default', DeprecationWarning)
+ return func(*args, **kwargs)
+ return decorated
+ return actual_decorator