Skip to content

Commit

Permalink
Massive typing fixes and ruff fixes. Still in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
xfenix committed Sep 25, 2023
1 parent 4620a6c commit 1870cbd
Show file tree
Hide file tree
Showing 15 changed files with 440 additions and 1,002 deletions.
1,102 changes: 274 additions & 828 deletions poetry.lock

Large diffs are not rendered by default.

23 changes: 17 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ license = "MIT"

[tool.poetry.dependencies]
python = "^3.10"
fastapi = {extras = ["all"], version = "*"}
gunicorn = "*"
uvicorn = "*"
loguru = "*"
Expand All @@ -17,9 +16,11 @@ pylru = "*"
aiopath = "*"
anyio = "*"
sentry-sdk = "*"
pydantic-settings = "^2.0.3"
pydantic-settings = "*"
fastapi = "*"

[tool.poetry.dev-dependencies]
[tool.poetry.group.dev.dependencies]
httpx = "*"
pytest = "*"
pytest-cov = "*"
pytest-xdist = "*"
Expand All @@ -41,7 +42,7 @@ lexicographical = true
sections = ["FUTURE", "STDLIB", "FIRSTPARTY", "THIRDPARTY", "LOCALFOLDER"]
no_lines_before = ["STDLIB", "THIRDPARTY"]
known_third_party = []
known_local_folder = ["whole_app",]
known_local_folder = ["whole_app"]

[tool.black]
line-length = 120
Expand All @@ -50,14 +51,24 @@ line-length = 120
fix = true
line-length = 120
select = ["ALL"]
ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D213", "FA102", "I"]
ignore = ["D1", "D203", "D213", "FA102", "I", "ANN101"]

[tool.ruff.extend-per-file-ignores]
"tests/*.py" = ["ANN001", "ANN002", "ANN003", "ANN401", "S101", "PLR2004", "S311"]
"tests/*.py" = [
"ANN001",
"ANN002",
"ANN003",
"ANN401",
"S101",
"PLR2004",
"S311",
]
"tests/_fixtures.py" = ["E501"]

[tool.mypy]
plugins = "pydantic.mypy"
ignore_missing_imports = true
strict = true

[tool.vulture]
exclude = ["whole_app/settings.py"]
Expand Down
19 changes: 15 additions & 4 deletions scripts/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,21 @@ def parse_last_git_tag() -> str:
"""Return last git tag (works in CI and on localhost)."""
last_tag_from_environment: typing.Final[str | None] = os.getenv("GITHUB_REF_NAME")
if last_tag_from_environment is None:
git_tags_list: typing.Final[list[str]] = shlex.split("git rev-list --tags --max-count=1")
last_tag_hash: typing.Final[str] = subprocess.check_output(git_tags_list).strip().decode() # noqa: S603
git_tag_description: typing.Final[list[str]] = shlex.split(f"git describe --tags {last_tag_hash}")
return subprocess.check_output(git_tag_description).strip().decode().lstrip("v") # noqa: S603
git_tags_list: typing.Final = shlex.split(
"git rev-list --tags --max-count=1",
)
last_tag_hash: typing.Final = (
subprocess.check_output(git_tags_list).strip().decode() # noqa: S603
)
git_tag_description: typing.Final = shlex.split(
f"git describe --tags {last_tag_hash}",
)
return (
subprocess.check_output(git_tag_description) # noqa: S603
.strip()
.decode()
.lstrip("v")
)
return last_tag_from_environment.lstrip("v")


Expand Down
9 changes: 5 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
"""Basic programmatic fixtures for views."""
import pathlib
import tempfile
import typing
Expand All @@ -13,20 +12,22 @@

@pytest.fixture(scope="session")
def faker_obj() -> faker.Faker:
"""Fixture for faker object."""
return faker.Faker("ru_RU")


@pytest.fixture(autouse=True)
def patch_file_provider_for_temp(monkeypatch) -> typing.Any:
def patch_file_provider_for_temp(monkeypatch) -> typing.Generator[None, None, None]:
"""Patch settings, to rewrite dict path to temporary directory."""
with monkeypatch.context() as patcher, tempfile.TemporaryDirectory() as tmp_dir_name:
yield patcher.setattr(SETTINGS, "dictionaries_path", pathlib.Path(tmp_dir_name))


# pylint: disable=redefined-outer-name
@pytest.fixture()
def app_client(monkeypatch: typing.Any, faker_obj: typing.Any) -> typing.Any:
def app_client(
monkeypatch: pytest.MonkeyPatch,
faker_obj: typing.Any,
) -> typing.Generator[TestClient, None, None]:
"""Fake client with patched fake storage.
Also in a form of context manager it allow us to test startup events
Expand Down
73 changes: 36 additions & 37 deletions tests/test_dict_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
from requests.models import Response as RequestsResponse


DICT_ENDPOINT: typing.Final[str] = f"{SETTINGS.api_prefix}/dictionaries/"
DICT_ENDPOINT: typing.Final = f"{SETTINGS.api_prefix}/dictionaries/"


class TestFileAndDummyBasedDicts:
"""Test file based user dict provider."""

@pytest.fixture(params=[StorageProviders.DUMMY, StorageProviders.FILE])
def patch_various_providers(self: typing.Self, monkeypatch, request) -> typing.Any:
"""Made test, used this fixture, run for various storage providers."""
def patch_various_providers(
self: "TestFileAndDummyBasedDicts",
monkeypatch: typing.Any,
request: typing.Any,
) -> typing.Any:
with monkeypatch.context() as patcher:
yield patcher.setattr(
SETTINGS,
Expand All @@ -31,18 +32,17 @@ def patch_various_providers(self: typing.Self, monkeypatch, request) -> typing.A

@pytest.mark.repeat(3)
def test_add_to_dict(
self: typing.Self,
app_client,
faker_obj,
patch_various_providers, # noqa: ARG002
self: "TestFileAndDummyBasedDicts",
app_client: typing.Any,
faker_obj: typing.Any,
patch_various_providers: typing.Any, # noqa: ARG002
) -> None:
"""Add to user dict."""
fake_user_name: typing.Final[str] = faker_obj.user_name()
fake_exc_word: typing.Final[str] = faker_obj.word()
path_to_dict_file: typing.Final[
str
] = SETTINGS.dictionaries_path.joinpath( # pylint: disable=no-member
fake_user_name,
fake_user_name: typing.Final = faker_obj.user_name()
fake_exc_word: typing.Final = faker_obj.word()
path_to_dict_file: typing.Final = (
SETTINGS.dictionaries_path.joinpath( # pylint: disable=no-member
fake_user_name,
)
)
server_response: RequestsResponse = app_client.post(
DICT_ENDPOINT,
Expand All @@ -57,18 +57,17 @@ def test_add_to_dict(

@pytest.mark.repeat(3)
def test_remove_from_user_dict(
self: typing.Self,
app_client,
faker_obj,
patch_various_providers, # noqa: ARG002
self: "TestFileAndDummyBasedDicts",
app_client: typing.Any,
faker_obj: typing.Any,
patch_various_providers: typing.Any, # noqa: ARG002
) -> None:
"""Delete from user dict."""
fake_exc_word: typing.Final[str] = faker_obj.word()
fake_user_name: typing.Final[str] = faker_obj.user_name()
path_to_dict_file: typing.Final[
str
] = SETTINGS.dictionaries_path.joinpath( # pylint: disable=no-member
fake_user_name,
fake_exc_word: typing.Final = faker_obj.word()
fake_user_name: typing.Final = faker_obj.user_name()
path_to_dict_file: typing.Final = (
SETTINGS.dictionaries_path.joinpath( # pylint: disable=no-member
fake_user_name,
)
)
path_to_dict_file.touch()
path_to_dict_file.write_text(fake_exc_word)
Expand All @@ -86,12 +85,11 @@ def test_remove_from_user_dict(
assert fake_exc_word not in path_to_dict_file.read_text()

def test_dummy_provider_init(
self: typing.Self,
monkeypatch,
app_client,
faker_obj,
self: "TestFileAndDummyBasedDicts",
monkeypatch: typing.Any,
app_client: typing.Any,
faker_obj: typing.Any,
) -> None:
"""Test init with dummy provider (through add test)."""
monkeypatch.setattr(
SETTINGS,
"dictionaries_storage_provider",
Expand All @@ -110,7 +108,10 @@ def test_dummy_provider_init(
class TestVarious:
"""Various dict api things."""

def test_disabled_dictionary_views(self: typing.Self, monkeypatch) -> None:
def test_disabled_dictionary_views(
self: "TestVarious",
monkeypatch: typing.Any,
) -> None:
"""Test views with dictionaries_disabled SETTINGS option."""
with monkeypatch.context() as patcher:
patcher.setattr(SETTINGS, "dictionaries_disabled", True)
Expand All @@ -127,8 +128,7 @@ def test_disabled_dictionary_views(self: typing.Self, monkeypatch) -> None:
importlib.reload(views)

@pytest.mark.parametrize("api_key", [None, ""])
def test_empty_auth_key(self: typing.Self, api_key) -> None:
"""Test add dict without api key at all."""
def test_empty_auth_key(self: "TestVarious", api_key: str) -> None:
server_response: RequestsResponse = TestClient(views.SPELL_APP).post(
DICT_ENDPOINT,
json=models.UserDictionaryRequestWithWord(
Expand All @@ -139,8 +139,7 @@ def test_empty_auth_key(self: typing.Self, api_key) -> None:
)
assert server_response.status_code == 403

def test_wrong_api_key(self: typing.Self) -> None:
"""Test add dict without api key at all."""
def test_wrong_api_key(self: "TestVarious") -> None:
server_response: RequestsResponse = TestClient(views.SPELL_APP).post(
DICT_ENDPOINT,
json=models.UserDictionaryRequestWithWord(
Expand Down
16 changes: 4 additions & 12 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,20 @@

if TYPE_CHECKING:
from requests.models import Response as RequestsResponse
import faker


def test_main_py(monkeypatch) -> None:
"""Test __main__.py."""

def test_main_py(monkeypatch: typing.Any) -> None:
class FakeGunicorn:
"""Fake gunicorn."""

def __init__(self: "FakeGunicorn", *_, **__) -> None:
"""Init."""

@property
def cfg(self: "FakeGunicorn") -> "FakeGunicorn":
"""Fake config object."""
return self

@property
def settings(self: "FakeGunicorn") -> dict[str, None | int]:
"""Fake settings object."""
return {
"bind": None,
"workers": 666_13,
Expand All @@ -41,16 +36,14 @@ def set(self: "FakeGunicorn", _, __) -> typing.Any: # noqa: A003
"""Fake setter for «config» object."""

def run(self: "FakeGunicorn", *_, **__) -> typing.Any:
"""Faky run."""
self.load_config()
self.load()

monkeypatch.setattr("gunicorn.app.base.BaseApplication", FakeGunicorn)
runpy.run_module("whole_app.__main__", run_name="__main__")


def test_incorrect_settings(monkeypatch) -> None:
"""Test some various incorrect settings."""
def test_incorrect_settings(monkeypatch: typing.Any) -> None:
fake_settings: SettingsOfMicroservice = SettingsOfMicroservice()
assert fake_settings.cache_size == 10_000

Expand All @@ -65,8 +58,7 @@ def test_incorrect_settings(monkeypatch) -> None:
assert fake_settings.current_version == ""


def test_sentry_integration(monkeypatch, faker_obj) -> None:
"""Test sentry integration."""
def test_sentry_integration(monkeypatch: typing.Any, faker_obj: "faker.Faker") -> None:
with monkeypatch.context() as patcher:
patcher.setattr(SETTINGS, "sentry_dsn", f"https://{faker_obj.pystr()}")
patcher.setattr("sentry_sdk.init", lambda **_: None)
Expand Down
31 changes: 18 additions & 13 deletions tests/test_spell_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,43 @@

if typing.TYPE_CHECKING:
from requests.models import Response as RequestsResponse
from fastapi.testclient import TestClient
import faker


RUSSIAN_LETTERS: typing.Final[str] = "абвгдежзийклмнопрстуфхцчшщъыьэюяё"
RU_LANG: typing.Final[str] = "ru_RU"
RUSSIAN_LETTERS: typing.Final = "абвгдежзийклмнопрстуфхцчшщъыьэюяё"
RU_LANG: typing.Final = "ru_RU"


@pytest.mark.parametrize(
"wannabe_user_input",
["Привет как дела", "Пока, я ушёл", *BAD_PAYLOAD],
)
def test_no_corrections(app_client, wannabe_user_input) -> None:
def test_no_corrections(app_client: "TestClient", wannabe_user_input: str) -> None:
"""Dead simple test."""
server_response: typing.Final[RequestsResponse] = app_client.post(
server_response: typing.Final = app_client.post(
f"{SETTINGS.api_prefix}/check/",
json=models.SpellCheckRequest(text=wannabe_user_input, language=RU_LANG).dict(),
)
assert server_response.status_code == 200


@pytest.mark.repeat(5)
def test_with_corrections_simple(app_client, faker_obj) -> None:
def test_with_corrections_simple(
app_client: "TestClient",
faker_obj: "faker.Faker",
) -> None:
"""Not so dead simple test."""
generated_letter: typing.Final[str] = random.choice(RUSSIAN_LETTERS)
wannabe_user_input: str = (
generated_letter: typing.Final = random.choice(RUSSIAN_LETTERS)
wannabe_user_input: typing.Final[str] = (
faker_obj.text()
.lower()
.replace(
generated_letter,
random.choice(RUSSIAN_LETTERS.replace(generated_letter, "")),
)
)
server_response: typing.Final[RequestsResponse] = app_client.post(
server_response: typing.Final = app_client.post(
f"{SETTINGS.api_prefix}/check/",
json=models.SpellCheckRequest(
text=wannabe_user_input,
Expand All @@ -62,11 +67,11 @@ def test_with_corrections_simple(app_client, faker_obj) -> None:
],
)
def test_with_exception_word_in_dictionary(
monkeypatch,
app_client,
faker_obj,
wannabe_user_input,
tested_word,
monkeypatch: typing.Any,
app_client: "TestClient",
faker_obj: "faker.Faker",
wannabe_user_input: str,
tested_word: str,
) -> None:
"""Complex tests, where we add word to dictionary and tests that it really excluded from the output."""
# replace all symbols from wannabe_user_input except letters and numbers
Expand Down
9 changes: 2 additions & 7 deletions whole_app/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
This file meant only for basic workers wrappers and fastapi exposure.
For end-points look in views.py
"""
import typing
import fastapi
from gunicorn.app.base import BaseApplication

Expand All @@ -13,10 +12,7 @@

# pylint: disable=abstract-method
class GunicornCustomApplication(BaseApplication):
"""Our easing wrapper around gunicorn."""

def load_config(self: typing.Self) -> None:
"""Load configuration from memory."""
def load_config(self) -> None:
_options: dict[str, str | int] = {
"worker_class": "uvicorn.workers.UvicornWorker",
"bind": f"0.0.0.0:{SETTINGS.port}",
Expand All @@ -26,8 +22,7 @@ def load_config(self: typing.Self) -> None:
if key in self.cfg.settings and value is not None:
self.cfg.set(key.lower(), value)

def load(self: typing.Self) -> fastapi.FastAPI:
"""Just return application."""
def load(self) -> fastapi.FastAPI:
return SPELL_APP


Expand Down
Loading

0 comments on commit 1870cbd

Please sign in to comment.