diff --git a/mimesis/__init__.py b/mimesis/__init__.py index b0fd9d7af..ace468fec 100644 --- a/mimesis/__init__.py +++ b/mimesis/__init__.py @@ -146,7 +146,7 @@ "__license__", ] -__version__ = "14.0.0" +__version__ = "15.0.0" __title__ = "mimesis" __description__ = "Mimesis: Fake Data Generator." __url__ = "https://github.com/lk-geimfari/mimesis" diff --git a/mimesis/plugins/factory.py b/mimesis/plugins/factory.py new file mode 100644 index 000000000..491a2a66f --- /dev/null +++ b/mimesis/plugins/factory.py @@ -0,0 +1,118 @@ +from contextlib import contextmanager +from typing import Any, ClassVar, Iterator + +from mimesis.locales import Locale +from mimesis.schema import Field, RegisterableFieldHandlers + +try: + from factory import declarations + from factory.builder import BuildStep, Resolver +except ImportError: + raise ImportError("This plugin requires factory_boy to be installed.") + +__all__ = ["MimesisField"] + + +class MimesisField(declarations.BaseDeclaration): # type: ignore[misc] + """ + Mimesis integration with FactoryBoy starts here. + + This class provides a common interface for FactoryBoy, + but inside it has Mimesis generators. + """ + + _default_locale: ClassVar[Locale] = Locale.EN + _cached_instances: ClassVar[dict[str, Field]] = {} + + def __init__( + self, + field: str, + locale: Locale | None = None, + **kwargs: Any, + ) -> None: + """ + Creates a field instance. + + The created field is lazy. It also receives build time parameters. + These parameters are not applied yet. + + :param field: name to be passed to :class:`~mimesis.schema.Field`. + :param locale: locale to use. This parameter has the highest priority. + :param kwargs: optional parameters that would be passed to ``Field``. + """ + super().__init__() + self.locale = locale + self.kwargs = kwargs + self.field = field + + def evaluate( + self, + instance: Resolver, + step: BuildStep, + extra: dict[str, Any] | None = None, + ) -> Any: + """Evaluates the lazy field. + + :param instance: (factory.builder.Resolver): The object holding currently computed attributes. + :param step: (factory.builder.BuildStep): The object holding the current build step. + :param extra: Extra call-time added kwargs that would be passed to ``Field``. + """ + kwargs: dict[str, Any] = {} + kwargs.update(self.kwargs) + kwargs.update(extra or {}) + + field_handlers = step.builder.factory_meta.declarations.get( + "field_handlers", [] + ) + + mimesis_field = self._get_cached_instance( + locale=self.locale, + field_handlers=field_handlers, + ) + return mimesis_field(self.field, **kwargs) + + @classmethod + @contextmanager + def override_locale(cls, locale: Locale) -> Iterator[None]: + """ + Overrides unspecified locales. + + Remember that implicit locales would not be overridden. + """ + old_locale = cls._default_locale + cls._default_locale = locale + yield + cls._default_locale = old_locale + + @classmethod + def _get_cached_instance( + cls, + locale: Locale | None = None, + field_handlers: RegisterableFieldHandlers | None = None, + ) -> Field: + """Returns cached instance. + + :param locale: locale to use. + :param field_handlers: custom field handlers. + :return: cached instance of Field. + """ + if locale is None: + locale = cls._default_locale + + field_names = "-".join( + sorted( + dict(field_handlers if field_handlers else []).keys(), + ) + ) + + key = f"{locale}{field_names}" + + if key not in cls._cached_instances: + field = Field(locale) + + if field_handlers: + field.register_handlers(field_handlers) + + cls._cached_instances[key] = field + + return cls._cached_instances[key] diff --git a/mimesis/schema.py b/mimesis/schema.py index 54fd874ff..3d3a4f60f 100644 --- a/mimesis/schema.py +++ b/mimesis/schema.py @@ -5,7 +5,7 @@ import json import pickle import re -import typing as t +from typing import Any, Callable, Sequence from mimesis.exceptions import ( AliasesTypeError, @@ -21,12 +21,20 @@ from mimesis.random import Random from mimesis.types import JSON, CallableSchema, Key, MissingSeed, Seed -__all__ = ["BaseField", "Field", "Fieldset", "Schema"] - -FieldCache = dict[str, t.Callable[[t.Any], t.Any]] -FieldHandler = t.Callable[[Random, t.Any], t.Any] +__all__ = [ + "BaseField", + "Field", + "Fieldset", + "Schema", + "FieldHandler", + "RegisterableFieldHandler", + "RegisterableFieldHandlers", +] + +FieldCache = dict[str, Callable[[Any], Any]] +FieldHandler = Callable[[Random, Any], Any] RegisterableFieldHandler = tuple[str, FieldHandler] -RegisterableFieldHandlers = t.Sequence[RegisterableFieldHandler] +RegisterableFieldHandlers = Sequence[RegisterableFieldHandler] class BaseField: @@ -62,7 +70,7 @@ def get_random_instance(self) -> Random: """ return self._generic.random - def _explicit_lookup(self, name: str) -> t.Any: + def _explicit_lookup(self, name: str) -> Any: """An explicit method lookup. This method is called when the field @@ -79,7 +87,7 @@ def _explicit_lookup(self, name: str) -> t.Any: except AttributeError: raise FieldError(name) - def _fuzzy_lookup(self, name: str) -> t.Any: + def _fuzzy_lookup(self, name: str) -> Any: """A fuzzy method lookup. This method is called when the field definition @@ -97,7 +105,7 @@ def _fuzzy_lookup(self, name: str) -> t.Any: raise FieldError(name) - def _lookup_method(self, name: str) -> t.Any: + def _lookup_method(self, name: str) -> Any: """Lookup method by the field name. :param name: The field name. @@ -137,8 +145,8 @@ def perform( self, name: str | None = None, key: Key = None, - **kwargs: t.Any, - ) -> t.Any: + **kwargs: Any, + ) -> Any: """Performs the value of the field by its name. It takes any string that represents the name of any method of @@ -224,7 +232,7 @@ def register_handler(self, field_name: str, field_handler: FieldHandler) -> None def handle( self, field_name: str | None = None - ) -> t.Callable[[FieldHandler], FieldHandler]: + ) -> Callable[[FieldHandler], FieldHandler]: """Decorator for registering a custom field handler. You can use this decorator only for functions, @@ -261,7 +269,7 @@ def unregister_handler(self, field_name: str) -> None: self._handlers.pop(field_name, None) - def unregister_handlers(self, field_names: t.Sequence[str] = ()) -> None: + def unregister_handlers(self, field_names: Sequence[str] = ()) -> None: """Unregister a field handlers with given names. :param field_names: Names of the fields. @@ -305,7 +313,7 @@ class Field(BaseField): Dogtag_1836 """ - def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + def __call__(self, *args: Any, **kwargs: Any) -> Any: return self.perform(*args, **kwargs) @@ -338,7 +346,7 @@ class Fieldset(BaseField): fieldset_default_iterations: int = 10 fieldset_iterations_kwarg: str = "i" - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize fieldset. Accepts additional keyword argument **i** which is used @@ -353,7 +361,7 @@ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: ) super().__init__(*args, **kwargs) - def __call__(self, *args: t.Any, **kwargs: t.Any) -> list[t.Any]: + def __call__(self, *args: Any, **kwargs: Any) -> list[Any]: """Perform fieldset. :param args: Arguments for field. @@ -399,7 +407,7 @@ def __init__(self, schema: CallableSchema, iterations: int = 10) -> None: else: raise SchemaError() - def to_csv(self, file_path: str, **kwargs: t.Any) -> None: + def to_csv(self, file_path: str, **kwargs: Any) -> None: """Export a schema as a CSV file. :param file_path: The file path. @@ -412,7 +420,7 @@ def to_csv(self, file_path: str, **kwargs: t.Any) -> None: dict_writer.writeheader() dict_writer.writerows(data) - def to_json(self, file_path: str, **kwargs: t.Any) -> None: + def to_json(self, file_path: str, **kwargs: Any) -> None: """Export a schema as a JSON file. :param file_path: File a path. @@ -421,7 +429,7 @@ def to_json(self, file_path: str, **kwargs: t.Any) -> None: with open(file_path, "w", encoding="utf-8") as fp: json.dump(self.create(), fp, **kwargs) - def to_pickle(self, file_path: str, **kwargs: t.Any) -> None: + def to_pickle(self, file_path: str, **kwargs: Any) -> None: """Export a schema as the pickled representation of the object to the file. :param file_path: The file path. diff --git a/poetry.lock b/poetry.lock index d1fa7fb60..7e391fdcf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -324,6 +324,38 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "factory-boy" +version = "3.3.0" +description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." +optional = false +python-versions = ">=3.7" +files = [ + {file = "factory_boy-3.3.0-py2.py3-none-any.whl", hash = "sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c"}, + {file = "factory_boy-3.3.0.tar.gz", hash = "sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1"}, +] + +[package.dependencies] +Faker = ">=0.7.0" + +[package.extras] +dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "sqlalchemy-utils", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] +doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] + +[[package]] +name = "faker" +version = "23.2.1" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Faker-23.2.1-py3-none-any.whl", hash = "sha256:0520a6b97e07c658b2798d7140971c1d5bc4bcd5013e7937fe075fd054aa320c"}, + {file = "Faker-23.2.1.tar.gz", hash = "sha256:f07b64d27f67b62c7f0536a72f47813015b3b51cd4664918454011094321e464"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + [[package]] name = "idna" version = "3.6" @@ -346,6 +378,17 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] +[[package]] +name = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +optional = false +python-versions = ">=3.5" +files = [ + {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, + {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -672,6 +715,23 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-factoryboy" +version = "2.6.0" +description = "Factory Boy support for pytest." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_factoryboy-2.6.0-py3-none-any.whl", hash = "sha256:23facf586a1beedea03e875159001bfeb8393fb56ab104d87ad06f688d269e5b"}, + {file = "pytest_factoryboy-2.6.0.tar.gz", hash = "sha256:d09c37178693d8e594a96faf3c56e870b7753d2622710298b850ef79eb02d63d"}, +] + +[package.dependencies] +factory_boy = ">=2.10.0" +inflection = "*" +pytest = ">=6.2" +typing_extensions = "*" + [[package]] name = "pytest-mock" version = "3.12.0" @@ -717,6 +777,20 @@ files = [ [package.dependencies] pytest = "*" +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pytz" version = "2023.3.post1" @@ -749,6 +823,17 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -1007,7 +1092,10 @@ decorator = ">=3.4.0" [package.extras] test = ["flake8 (>=2.4.0)", "isort (>=4.2.2)", "pytest (>=2.2.3)"] +[extras] +factory = [] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "82f479d4bea9aecd9e2e4d3e207b627d9509fb080b51439fb6210ae4e64c53d8" +content-hash = "f7819564d9d5e7512da6691f5904a5a6bbdc5fe9dde87a4e5281bf855897ec7e" diff --git a/pyproject.toml b/pyproject.toml index 8ffcccb78..85d3f7335 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,11 @@ pytest-cov = "^4.0.0" Sphinx = ">=5.1.1,<8.0.0" sphinx-copybutton = "^0.5.0" sphinx-autodoc-typehints = "^1.19.2" +factory-boy = "^3.3.0" +pytest-factoryboy = "^2.6.0" + +[tool.poetry.extras] +factory = ["factory_boy"] [tool.poetry.plugins."pytest_randomly.random_seeder"] mimesis = "mimesis.entrypoints:pytest_randomly_reseed" diff --git a/tests/test_plugins/test_factory/__init__.py b/tests/test_plugins/test_factory/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_plugins/test_factory/test_field_handlers.py b/tests/test_plugins/test_factory/test_field_handlers.py new file mode 100644 index 000000000..f9084a8d1 --- /dev/null +++ b/tests/test_plugins/test_factory/test_field_handlers.py @@ -0,0 +1,46 @@ +import factory +import pytest +from pytest_factoryboy import register + +from mimesis.exceptions import FieldError +from mimesis.plugins.factory import MimesisField + + +class Guest: + def __init__(self, full_name: str, age: int) -> None: + self.age = age + self.full_name = full_name + + +@register +class FactoryWithoutCustomFieldHandlers(factory.Factory): + class Meta(object): + model = Guest + + age = MimesisField("anynum") + full_name = MimesisField("nickname") + + +@register +class FactoryWithCustomFieldHandlers(factory.Factory): + class Meta(object): + model = Guest + + class Params(object): + field_handlers = [ + ("anynum", lambda rand, **kwargs: rand.randint(1, 99)), + ("nickname", lambda rand, **kwargs: rand.choice(["john", "alice"])), + ] + + age = MimesisField("anynum") + full_name = MimesisField("nickname") + + +def test_factory_without_custom_field_handlers(factory_without_custom_field_handlers): + with pytest.raises(FieldError): + factory_without_custom_field_handlers() + + +def test_factory_with_custom_field_handlers(factory_with_custom_field_handlers): + guest = factory_with_custom_field_handlers() + assert isinstance(guest, Guest) diff --git a/tests/test_plugins/test_factory/test_field_params.py b/tests/test_plugins/test_factory/test_field_params.py new file mode 100644 index 000000000..e66528fe3 --- /dev/null +++ b/tests/test_plugins/test_factory/test_field_params.py @@ -0,0 +1,71 @@ +import factory +import pytest +from pytest_factoryboy import register + +from mimesis.enums import Gender +from mimesis.plugins.factory import MimesisField + +MIN_AGE = 30 +MAX_AGE = 32 + + +class Guest(object): + def __init__(self, full_name, age): + self.full_name = full_name + self.age = age + + +@register +class GuestFactory(factory.Factory): + class Meta(object): + model = Guest + + full_name = MimesisField("full_name", gender=Gender.FEMALE) + age = MimesisField("integer_number", start=MIN_AGE, end=MAX_AGE) + + +def test_guest_factory_different_data(guest_factory): + guest1 = guest_factory() + guest2 = guest_factory() + + assert isinstance(guest1, Guest) + assert isinstance(guest2, Guest) + assert guest1 != guest2 + assert guest1.full_name != guest2.full_name + assert MIN_AGE <= guest1.age <= MAX_AGE + assert MIN_AGE <= guest2.age <= MAX_AGE + + +def test_guest_factory_create_batch(guest_factory): + guests = guest_factory.create_batch(50) + names = {guest.full_name for guest in guests} + + assert len(guests) == len(names) + + for guest in guests: + assert isinstance(guest, Guest) + assert MIN_AGE <= guest.age <= MAX_AGE + + +def test_guest_factory_build_batch(guest_factory): + guests = guest_factory.build_batch(50) + names = {guest.full_name for guest in guests} + + assert len(guests) == len(names) + + for guest in guests: + assert isinstance(guest, Guest) + assert MIN_AGE <= guest.age <= MAX_AGE + + +def test_guest_instance_data(guest): + assert isinstance(guest, Guest) + assert guest.full_name != "" + assert MIN_AGE <= guest.age <= MAX_AGE + + +@pytest.mark.parametrize("guest__age", [19]) +def test_guest_data_overrides(guest): + assert isinstance(guest, Guest) + assert guest.full_name != "" + assert guest.age == 19 diff --git a/tests/test_plugins/test_factory/test_locale_override.py b/tests/test_plugins/test_factory/test_locale_override.py new file mode 100644 index 000000000..dc9064138 --- /dev/null +++ b/tests/test_plugins/test_factory/test_locale_override.py @@ -0,0 +1,64 @@ +import string + +import factory +from pytest_factoryboy import register + +from mimesis.locales import Locale +from mimesis.plugins.factory import MimesisField + + +class Person(object): + def __init__(self, full_name_en: str, full_name_ru: str) -> None: + self._full_name_en = full_name_en + self._full_name_ru = full_name_ru + + @property + def full_name_en(self) -> str: + """Some names have special symbols in them.""" + return self._full_name_en.replace(" ", "").replace("'", "") + + @property + def full_name_ru(self) -> str: + """Some names have special symbols in them.""" + return self._full_name_ru.replace(" ", "").replace("'", "") + + +@register +class PersonFactory(factory.Factory): + class Meta(object): + model = Person + + full_name_en = MimesisField("full_name") + full_name_ru = MimesisField("full_name", locale=Locale.RU) + + +def test_data_with_different_locales(person): + for letter in person.full_name_en: + assert letter in string.ascii_letters + + for russian_letter in person.full_name_ru: + assert russian_letter not in string.ascii_letters + + +def test_data_with_override_locale(person_factory): + with MimesisField.override_locale(Locale.RU): + person = person_factory() + + for letter in person.full_name_en: + # Default locale will be changed to overridden: + assert letter not in string.ascii_letters + + for russian_letter in person.full_name_ru: + assert russian_letter not in string.ascii_letters + + +def test_data_with_override_defined_locale(person_factory): + with MimesisField.override_locale(Locale.EN): + person = person_factory() + + for letter in person.full_name_en: + assert letter in string.ascii_letters + + for russian_letter in person.full_name_ru: + # Keyword locale has a priority over override: + assert russian_letter not in string.ascii_letters diff --git a/tests/test_plugins/test_factory/test_raw_factories.py b/tests/test_plugins/test_factory/test_raw_factories.py new file mode 100644 index 000000000..cad970ad6 --- /dev/null +++ b/tests/test_plugins/test_factory/test_raw_factories.py @@ -0,0 +1,33 @@ +import factory + +from mimesis.plugins.factory import MimesisField + + +class User(object): + def __init__(self, uid, email): + self.uid = uid + self.email = email + + +class UserFactory(factory.Factory): + class Meta(object): + model = User + + uid = factory.Sequence(lambda order: order) + email = MimesisField("email") + + +def test_direct_factory(): + users = UserFactory.create_batch(10) + + uids = {user.uid for user in users} + emails = {user.email for user in users} + + assert len(users) == len(emails) + assert len(users) == len(uids) + + +def test_factory_extras(): + user = UserFactory(email="custom@mail.ru") + + assert user.email == "custom@mail.ru" diff --git a/tests/test_plugins/test_factory/test_raw_objects.py b/tests/test_plugins/test_factory/test_raw_objects.py new file mode 100644 index 000000000..4026ab3e8 --- /dev/null +++ b/tests/test_plugins/test_factory/test_raw_objects.py @@ -0,0 +1,127 @@ +import factory +import pytest +import validators +from pytest_factoryboy import register + +from mimesis.plugins.factory import MimesisField + +TEST_USERNAMES = ("sobolevn", "lk-geimfari") + + +class Account(object): + def __init__(self, uid, username, email): + self.uid = uid + self.username = username + self.email = email + + +@register +class AccountFactory(factory.Factory): + class Meta(object): + model = Account + exclude = ("_domain",) + + uid = factory.Sequence(lambda order: order + 1) + username = MimesisField("username") + _domain = MimesisField("top_level_domain") + email = factory.LazyAttribute( + lambda instance: "{0}@example{1}".format( + instance.username, + instance._domain, # noqa: WPS437 + ), + ) + + +def test_account_factory_different_data(account_factory): + account1 = account_factory() + account2 = account_factory() + + assert isinstance(account1, Account) + assert isinstance(account2, Account) + assert account1 != account2 + assert account1.uid != account2.uid + assert account1.username != account2.username + assert account1.email != account2.email + + +def test_account_factory_overrides(account_factory): + username = "sobolevn" + desired_id = 190 + account = account_factory(username=username, uid=desired_id) + + assert account.uid == desired_id + assert account.username == username + assert account.email.startswith(username) + + +def test_account_factory_create_batch(account_factory): + accounts = account_factory.create_batch(10) + uids = {account.uid for account in accounts} + usernames = {account.username for account in accounts} + + assert len(accounts) == len(uids) + assert len(accounts) == len(usernames) + + for account in accounts: + assert isinstance(account, Account) + assert account.uid > 0 + assert account.username != "" + assert account.email.startswith(account.username) + + +def test_account_factory_build_batch(account_factory): + accounts = account_factory.build_batch(10) + uids = {account.uid for account in accounts} + usernames = {account.username for account in accounts} + + assert len(accounts) == len(uids) + assert len(accounts) == len(usernames) + + for account in accounts: + assert isinstance(account, Account) + assert account.uid > 0 + assert account.username != "" + assert account.email.startswith(account.username) + + +def test_account_data(account): + assert isinstance(account, Account) + assert account.uid > 0 + assert account.username != "" + assert validators.email(account.email) + + username, domain = account.email.split("@") + assert account.username == username + assert validators.domain(domain) + + +@pytest.mark.parametrize("account__username", TEST_USERNAMES) +def test_account_data_overrides(account): + assert account.username in TEST_USERNAMES + + username, _ = account.email.split("@") + + assert account.username == username + assert username in TEST_USERNAMES + + +@pytest.mark.parametrize( + ("account__username", "account__uid"), + zip( + TEST_USERNAMES, + range(10000, 10003), + ), +) +def test_account_multiple_data_overrides(account): + assert account.username in TEST_USERNAMES + assert 10000 <= account.uid < 10003 + + username, _ = account.email.split("@") + + assert account.username == username + assert username in TEST_USERNAMES + + +def test_account_excluded_data(account): + with pytest.raises(AttributeError): + account._domain # noqa: WPS428, WPS437