Compare commits

...

11 Commits

18 changed files with 333 additions and 59 deletions

View File

@@ -1,9 +1,6 @@
__name__ = "libbot" __version__ = "0.2.0"
__version__ = "1.9"
__license__ = "GPL3" __license__ = "GPL3"
__author__ = "Profitroll" __author__ = "Profitroll"
from . import i18n, pyrogram, sync
from .__main__ import * from .__main__ import *
from . import sync
from . import i18n
from . import pyrogram

View File

@@ -2,14 +2,15 @@ from os import listdir
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Union from typing import Any, Dict, Union
from libbot import config_get, json_read, sync import libbot
from libbot.i18n import sync
from libbot.i18n.classes.bot_locale import BotLocale from libbot.i18n.classes.bot_locale import BotLocale
async def _( async def _(
key: str, key: str,
*args: str, *args: str,
locale: str = sync.config_get("locale"), locale: Union[str, None] = "en",
locales_root: Union[str, Path] = Path("locale"), locales_root: Union[str, Path] = Path("locale"),
) -> Any: ) -> Any:
"""Get value of locale string """Get value of locale string
@@ -17,20 +18,20 @@ async def _(
### Args: ### Args:
* key (`str`): The last key of the locale's keys path. * key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: `dict[args][key]`. * *args (`list`): Path to key like: `dict[args][key]`.
* locale (`str`): Locale to looked up in. Defaults to config's `"locale"` value. * locale (`Union[str, None]`): Locale to looked up in. Defaults to `"en"`.
* locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`. * locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`.
### Returns: ### Returns:
* `Any`: Value of provided locale key. Is usually `str`, `dict` or `list` * `Any`: Value of provided locale key. Is usually `str`, `dict` or `list`
""" """
locale = sync.config_get("locale") if locale is None else locale locale = libbot.sync.config_get("locale") if locale is None else locale
try: try:
this_dict = await json_read(Path(f"{locales_root}/{locale}.json")) this_dict = await libbot.json_read(Path(f"{locales_root}/{locale}.json"))
except FileNotFoundError: except FileNotFoundError:
try: try:
this_dict = await json_read( this_dict = await libbot.json_read(
Path(f'{locales_root}/{await config_get("locale")}.json') Path(f'{locales_root}/{await libbot.config_get("locale")}.json')
) )
except FileNotFoundError: except FileNotFoundError:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"' return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"'
@@ -65,7 +66,7 @@ async def in_all_locales(
valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales] valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales]
for lc in valid_locales: for lc in valid_locales:
try: try:
this_dict = await json_read(Path(f"{locales_root}/{lc}.json")) this_dict = await libbot.json_read(Path(f"{locales_root}/{lc}.json"))
except FileNotFoundError: except FileNotFoundError:
continue continue
@@ -101,7 +102,7 @@ async def in_every_locale(
valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales] valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales]
for lc in valid_locales: for lc in valid_locales:
try: try:
this_dict = await json_read(Path(f"{locales_root}/{lc}.json")) this_dict = await libbot.json_read(Path(f"{locales_root}/{lc}.json"))
except FileNotFoundError: except FileNotFoundError:
continue continue

View File

@@ -10,6 +10,7 @@ class BotLocale:
def __init__( def __init__(
self, self,
default_locale: Union[str, None] = "en",
locales_root: Union[str, Path] = Path("locale"), locales_root: Union[str, Path] = Path("locale"),
) -> None: ) -> None:
if isinstance(locales_root, str): if isinstance(locales_root, str):
@@ -23,7 +24,9 @@ class BotLocale:
".".join(entry.split(".")[:-1]) for entry in files_locales ".".join(entry.split(".")[:-1]) for entry in files_locales
] ]
self.default: str = sync.config_get("locale") self.default: str = (
sync.config_get("locale") if default_locale is None else default_locale
)
self.locales: dict = {} self.locales: dict = {}
for lc in valid_locales: for lc in valid_locales:

View File

@@ -2,14 +2,13 @@ from os import listdir
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Union from typing import Any, Dict, Union
from libbot import sync import libbot
from libbot.sync import config_get, json_read
def _( def _(
key: str, key: str,
*args: str, *args: str,
locale: str = sync.config_get("locale"), locale: Union[str, None] = "en",
locales_root: Union[str, Path] = Path("locale"), locales_root: Union[str, Path] = Path("locale"),
) -> Any: ) -> Any:
"""Get value of locale string """Get value of locale string
@@ -17,20 +16,22 @@ def _(
### Args: ### Args:
* key (`str`): The last key of the locale's keys path. * key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: `dict[args][key]`. * *args (`list`): Path to key like: `dict[args][key]`.
* locale (`str`): Locale to looked up in. Defaults to config's `"locale"` value. * locale (`Union[str, None]`): Locale to looked up in. Defaults to `"en"`.
* locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`. * locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`.
### Returns: ### Returns:
* `Any`: Value of provided locale key. Is usually `str`, `dict` or `list` * `Any`: Value of provided locale key. Is usually `str`, `dict` or `list`
""" """
if locale is None: if locale is None:
locale = sync.config_get("locale") locale = libbot.sync.config_get("locale")
try: try:
this_dict = json_read(Path(f"{locales_root}/{locale}.json")) this_dict = libbot.sync.json_read(Path(f"{locales_root}/{locale}.json"))
except FileNotFoundError: except FileNotFoundError:
try: try:
this_dict = json_read(Path(f'{locales_root}/{config_get("locale")}.json')) this_dict = libbot.sync.json_read(
Path(f'{locales_root}/{libbot.sync.config_get("locale")}.json')
)
except FileNotFoundError: except FileNotFoundError:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"' return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"'
@@ -64,7 +65,7 @@ def in_all_locales(
valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales] valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales]
for lc in valid_locales: for lc in valid_locales:
try: try:
this_dict = json_read(Path(f"{locales_root}/{lc}.json")) this_dict = libbot.sync.json_read(Path(f"{locales_root}/{lc}.json"))
except FileNotFoundError: except FileNotFoundError:
continue continue
@@ -100,7 +101,7 @@ def in_every_locale(
valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales] valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales]
for lc in valid_locales: for lc in valid_locales:
try: try:
this_dict = json_read(Path(f"{locales_root}/{lc}.json")) this_dict = libbot.sync.json_read(Path(f"{locales_root}/{lc}.json"))
except FileNotFoundError: except FileNotFoundError:
continue continue

View File

@@ -55,7 +55,9 @@ def nested_set(target: dict, value: Any, *path: str, create_missing=True) -> dic
elif create_missing: elif create_missing:
d = d.setdefault(key, {}) d = d.setdefault(key, {})
else: else:
return target raise KeyError(
f"Key '{key}' is not found under path provided ({path}) and create_missing is False"
)
if path[-1] in d or create_missing: if path[-1] in d or create_missing:
d[path[-1]] = value d[path[-1]] = value
return target return target

View File

@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "libbot" name = "libbot"
version = "1.9" version = "0.2.0"
authors = [{ name = "Profitroll" }] authors = [{ name = "Profitroll" }]
description = "Universal bot library with functions needed for basic Discord/Telegram bot development." description = "Universal bot library with functions needed for basic Discord/Telegram bot development."
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
license = { text = "GPL3" } license = { file = "LICENSE" }
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Intended Audience :: Developers", "Intended Audience :: Developers",
@@ -52,3 +52,9 @@ target-version = ['py38', 'py39', 'py310', 'py311']
[tool.isort] [tool.isort]
profile = "black" profile = "black"
[tool.pytest.ini_options]
minversion = "6.0"
python_files = ["test_*.py"]
pythonpath = "."
testpaths = ["tests"]

View File

@@ -1,3 +0,0 @@
[metadata]
description_file=README.md
license_files=LICENSE

View File

@@ -1,4 +0,0 @@
from setuptools import setup
if __name__ == "__main__":
setup()

11
tests/data/locale/en.json Normal file
View File

@@ -0,0 +1,11 @@
{
"foo": "bar",
"messages": {
"example": "okay"
},
"callbacks": {
"default": {
"nested": "sure"
}
}
}

11
tests/data/locale/uk.json Normal file
View File

@@ -0,0 +1,11 @@
{
"foo": "бар",
"messages": {
"example": "окей"
},
"callbacks": {
"default": {
"nested": "авжеж"
}
}
}

53
tests/test_bot_locale.py Normal file
View File

@@ -0,0 +1,53 @@
from pathlib import Path
from typing import Any, List, Union
import pytest
from libbot.i18n import BotLocale
bot_locale = BotLocale(Path("tests/data/locale"))
@pytest.mark.parametrize(
"key, args, locale, expected",
[
("foo", [], None, "bar"),
("foo", [], "uk", "бар"),
("example", ["messages"], None, "okay"),
("example", ["messages"], "uk", "окей"),
("nested", ["callbacks", "default"], None, "sure"),
("nested", ["callbacks", "default"], "uk", "авжеж"),
],
)
def test_bot_locale_get(
key: str, args: List[str], locale: Union[str, None], expected: Any
):
assert (
bot_locale._(key, *args, locale=locale)
if locale is not None
else bot_locale._(key, *args)
) == expected
@pytest.mark.parametrize(
"key, args, expected",
[
("foo", [], ["bar", "бар"]),
("example", ["messages"], ["okay", "окей"]),
("nested", ["callbacks", "default"], ["sure", "авжеж"]),
],
)
def test_i18n_in_all_locales(key: str, args: List[str], expected: Any):
assert (bot_locale.in_all_locales(key, *args)) == expected
@pytest.mark.parametrize(
"key, args, expected",
[
("foo", [], {"en": "bar", "uk": "бар"}),
("example", ["messages"], {"en": "okay", "uk": "окей"}),
("nested", ["callbacks", "default"], {"en": "sure", "uk": "авжеж"}),
],
)
def test_i18n_in_every_locale(key: str, args: List[str], expected: Any):
assert (bot_locale.in_every_locale(key, *args)) == expected

View File

@@ -1,3 +1,4 @@
from pathlib import Path
from typing import Any, List from typing import Any, List
import pytest import pytest
@@ -14,7 +15,10 @@ from libbot import config_get, config_set
], ],
) )
async def test_config_get_valid(args: List[str], expected: str): async def test_config_get_valid(args: List[str], expected: str):
assert await config_get(args[0], *args[1:]) == expected assert (
await config_get(args[0], *args[1:], config_file=Path("tests/config.json"))
== expected
)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -26,7 +30,10 @@ async def test_config_get_valid(args: List[str], expected: str):
) )
async def test_config_get_invalid(args: List[str], expected: Any): async def test_config_get_invalid(args: List[str], expected: Any):
with expected: with expected:
assert await config_get(args[0], *args[1:]) == expected assert (
await config_get(args[0], *args[1:], config_file=Path("tests/config.json"))
== expected
)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -38,5 +45,5 @@ async def test_config_get_invalid(args: List[str], expected: Any):
], ],
) )
async def test_config_set(key: str, path: List[str], value: Any): async def test_config_set(key: str, path: List[str], value: Any):
await config_set(key, value, *path) await config_set(key, value, *path, config_file=Path("tests/config.json"))
assert await config_get(key, *path) == value assert await config_get(key, *path, config_file=Path("tests/config.json")) == value

View File

@@ -1,3 +1,4 @@
from pathlib import Path
from typing import Any, List from typing import Any, List
import pytest import pytest
@@ -13,7 +14,10 @@ from libbot import sync
], ],
) )
def test_config_get_valid(args: List[str], expected: str): def test_config_get_valid(args: List[str], expected: str):
assert sync.config_get(args[0], *args[1:]) == expected assert (
sync.config_get(args[0], *args[1:], config_file=Path("tests/config.json"))
== expected
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -24,7 +28,10 @@ def test_config_get_valid(args: List[str], expected: str):
) )
def test_config_get_invalid(args: List[str], expected: Any): def test_config_get_invalid(args: List[str], expected: Any):
with expected: with expected:
assert sync.config_get(args[0], *args[1:]) == expected assert (
sync.config_get(args[0], *args[1:], config_file=Path("tests/config.json"))
== expected
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -35,5 +42,5 @@ def test_config_get_invalid(args: List[str], expected: Any):
], ],
) )
def test_config_set(key: str, path: List[str], value: Any): def test_config_set(key: str, path: List[str], value: Any):
sync.config_set(key, value, *path) sync.config_set(key, value, *path, config_file=Path("tests/config.json"))
assert sync.config_get(key, *path) == value assert sync.config_get(key, *path, config_file=Path("tests/config.json")) == value

58
tests/test_i18n_async.py Normal file
View File

@@ -0,0 +1,58 @@
from pathlib import Path
from typing import Any, List, Union
import pytest
from libbot import i18n
@pytest.mark.asyncio
@pytest.mark.parametrize(
"key, args, locale, expected",
[
("foo", [], None, "bar"),
("foo", [], "uk", "бар"),
("example", ["messages"], None, "okay"),
("example", ["messages"], "uk", "окей"),
("nested", ["callbacks", "default"], None, "sure"),
("nested", ["callbacks", "default"], "uk", "авжеж"),
],
)
async def test_i18n_get(
key: str, args: List[str], locale: Union[str, None], expected: Any
):
assert (
await i18n._(key, *args, locale=locale, locales_root=Path("tests/data/locale"))
if locale is not None
else await i18n._(key, *args, locales_root=Path("tests/data/locale"))
) == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"key, args, expected",
[
("foo", [], ["bar", "бар"]),
("example", ["messages"], ["okay", "окей"]),
("nested", ["callbacks", "default"], ["sure", "авжеж"]),
],
)
async def test_i18n_in_all_locales(key: str, args: List[str], expected: Any):
assert (
await i18n.in_all_locales(key, *args, locales_root=Path("tests/data/locale"))
) == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"key, args, expected",
[
("foo", [], {"en": "bar", "uk": "бар"}),
("example", ["messages"], {"en": "okay", "uk": "окей"}),
("nested", ["callbacks", "default"], {"en": "sure", "uk": "авжеж"}),
],
)
async def test_i18n_in_every_locale(key: str, args: List[str], expected: Any):
assert (
await i18n.in_every_locale(key, *args, locales_root=Path("tests/data/locale"))
) == expected

53
tests/test_i18n_sync.py Normal file
View File

@@ -0,0 +1,53 @@
from pathlib import Path
from typing import Any, List, Union
import pytest
from libbot.i18n import sync
@pytest.mark.parametrize(
"key, args, locale, expected",
[
("foo", [], None, "bar"),
("foo", [], "uk", "бар"),
("example", ["messages"], None, "okay"),
("example", ["messages"], "uk", "окей"),
("nested", ["callbacks", "default"], None, "sure"),
("nested", ["callbacks", "default"], "uk", "авжеж"),
],
)
def test_i18n_get(key: str, args: List[str], locale: Union[str, None], expected: Any):
assert (
sync._(key, *args, locale=locale, locales_root=Path("tests/data/locale"))
if locale is not None
else sync._(key, *args, locales_root=Path("tests/data/locale"))
) == expected
@pytest.mark.parametrize(
"key, args, expected",
[
("foo", [], ["bar", "бар"]),
("example", ["messages"], ["okay", "окей"]),
("nested", ["callbacks", "default"], ["sure", "авжеж"]),
],
)
def test_i18n_in_all_locales(key: str, args: List[str], expected: Any):
assert (
sync.in_all_locales(key, *args, locales_root=Path("tests/data/locale"))
) == expected
@pytest.mark.parametrize(
"key, args, expected",
[
("foo", [], {"en": "bar", "uk": "бар"}),
("example", ["messages"], {"en": "okay", "uk": "окей"}),
("nested", ["callbacks", "default"], {"en": "sure", "uk": "авжеж"}),
],
)
def test_i18n_in_every_locale(key: str, args: List[str], expected: Any):
assert (
sync.in_every_locale(key, *args, locales_root=Path("tests/data/locale"))
) == expected

View File

@@ -1,4 +1,8 @@
from json import JSONDecodeError, dumps try:
from ujson import JSONDecodeError, dumps
except ImportError:
from json import dumps, JSONDecodeError
from pathlib import Path from pathlib import Path
from typing import Any, Union from typing import Any, Union
@@ -12,13 +16,13 @@ from libbot import json_read, json_write
"path, expected", "path, expected",
[ [
( (
"data/test.json", "tests/data/test.json",
{ {
"foo": "bar", "foo": "bar",
"abcdefg": ["higklmnop", {"lol": {"kek": [1.0000035, 0.2542, 1337]}}], "abcdefg": ["higklmnop", {"lol": {"kek": [1.0000035, 0.2542, 1337]}}],
}, },
), ),
("data/empty.json", {}), ("tests/data/empty.json", {}),
], ],
) )
async def test_json_read_valid(path: Union[str, Path], expected: Any): async def test_json_read_valid(path: Union[str, Path], expected: Any):
@@ -29,13 +33,13 @@ async def test_json_read_valid(path: Union[str, Path], expected: Any):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path, expected", "path, expected",
[ [
("data/invalid.json", pytest.raises(JSONDecodeError)), ("tests/data/invalid.json", JSONDecodeError),
("data/nonexistent.json", pytest.raises(FileNotFoundError)), ("tests/data/nonexistent.json", FileNotFoundError),
], ],
) )
async def test_json_read_invalid(path: Union[str, Path], expected: Any): async def test_json_read_invalid(path: Union[str, Path], expected: Any):
with expected: with pytest.raises(expected):
assert await json_read(path) == expected await json_read(path)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -47,9 +51,9 @@ async def test_json_read_invalid(path: Union[str, Path], expected: Any):
"foo": "bar", "foo": "bar",
"abcdefg": ["higklmnop", {"lol": {"kek": [1.0000035, 0.2542, 1337]}}], "abcdefg": ["higklmnop", {"lol": {"kek": [1.0000035, 0.2542, 1337]}}],
}, },
"data/test.json", "tests/data/test.json",
), ),
({}, "data/empty.json"), ({}, "tests/data/empty.json"),
], ],
) )
async def test_json_write(data: Any, path: Union[str, Path]): async def test_json_write(data: Any, path: Union[str, Path]):

View File

@@ -1,4 +1,8 @@
from json import JSONDecodeError, dumps try:
from ujson import JSONDecodeError, dumps
except ImportError:
from json import dumps, JSONDecodeError
from pathlib import Path from pathlib import Path
from typing import Any, Union from typing import Any, Union
@@ -11,13 +15,13 @@ from libbot import sync
"path, expected", "path, expected",
[ [
( (
"data/test.json", "tests/data/test.json",
{ {
"foo": "bar", "foo": "bar",
"abcdefg": ["higklmnop", {"lol": {"kek": [1.0000035, 0.2542, 1337]}}], "abcdefg": ["higklmnop", {"lol": {"kek": [1.0000035, 0.2542, 1337]}}],
}, },
), ),
("data/empty.json", {}), ("tests/data/empty.json", {}),
], ],
) )
def test_json_read_valid(path: Union[str, Path], expected: Any): def test_json_read_valid(path: Union[str, Path], expected: Any):
@@ -27,12 +31,12 @@ def test_json_read_valid(path: Union[str, Path], expected: Any):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path, expected", "path, expected",
[ [
("data/invalid.json", pytest.raises(JSONDecodeError)), ("tests/data/invalid.json", JSONDecodeError),
("data/nonexistent.json", pytest.raises(FileNotFoundError)), ("tests/data/nonexistent.json", FileNotFoundError),
], ],
) )
def test_json_read_invalid(path: Union[str, Path], expected: Any): def test_json_read_invalid(path: Union[str, Path], expected: Any):
with expected: with pytest.raises(expected):
assert sync.json_read(path) == expected assert sync.json_read(path) == expected
@@ -44,9 +48,9 @@ def test_json_read_invalid(path: Union[str, Path], expected: Any):
"foo": "bar", "foo": "bar",
"abcdefg": ["higklmnop", {"lol": {"kek": [1.0000035, 0.2542, 1337]}}], "abcdefg": ["higklmnop", {"lol": {"kek": [1.0000035, 0.2542, 1337]}}],
}, },
"data/test.json", "tests/data/test.json",
), ),
({}, "data/empty.json"), ({}, "tests/data/empty.json"),
], ],
) )
def test_json_write(data: Any, path: Union[str, Path]): def test_json_write(data: Any, path: Union[str, Path]):

63
tests/test_nested_set.py Normal file
View File

@@ -0,0 +1,63 @@
from typing import Any, List
import pytest
from libbot import sync
@pytest.mark.parametrize(
"target, value, path, create_missing, expected",
[
({"foo": "bar"}, "rab", ["foo"], True, {"foo": "rab"}),
({"foo": "bar"}, {"123": 456}, ["foo"], True, {"foo": {"123": 456}}),
(
{"foo": {"bar": {}}},
True,
["foo", "bar", "test"],
True,
{"foo": {"bar": {"test": True}}},
),
(
{"foo": {"bar": {}}},
True,
["foo", "bar", "test"],
False,
{"foo": {"bar": {}}},
),
],
)
def test_nested_set_valid(
target: dict[str, Any],
value: Any,
path: List[str],
create_missing: bool,
expected: Any,
):
assert (
sync.nested_set(target, value, *path, create_missing=create_missing)
) == expected
@pytest.mark.parametrize(
"target, value, path, create_missing, expected",
[
(
{"foo": {"bar": {}}},
True,
["foo", "bar", "test1", "test2"],
False,
KeyError,
),
],
)
def test_nested_set_invalid(
target: dict[str, Any],
value: Any,
path: List[str],
create_missing: bool,
expected: Any,
):
with pytest.raises(expected):
assert (
sync.nested_set(target, value, *path, create_missing=create_missing)
) == expected