From eb20911091e7de9bee0d20b968dc21a733e16d80 Mon Sep 17 00:00:00 2001 From: Hiroki KIYOHARA Date: Fri, 15 Nov 2024 11:50:06 +0900 Subject: [PATCH 1/4] Removed old Lint() class --- shodo/aio/lint.py | 6 ++-- shodo/lint.py | 38 +++-------------------- shodo/main.py | 16 ++++------ tests/test_main.py | 76 ++++++++++++++++++++++++---------------------- 4 files changed, 53 insertions(+), 83 deletions(-) diff --git a/shodo/aio/lint.py b/shodo/aio/lint.py index 23044d7..c9f7733 100644 --- a/shodo/aio/lint.py +++ b/shodo/aio/lint.py @@ -11,9 +11,9 @@ async def lint(body: str, is_html: bool = False, profile: Optional[str] = None) async with aiohttp.ClientSession() as session: create_res = await lint_create(body, is_html, profile, session) - status = Lint.STATUS_PROCESSING + status = Lint.STATUS_PROCESSING.value pause = 0.25 - while status == Lint.STATUS_PROCESSING: + while status == Lint.STATUS_PROCESSING.value: await asyncio.sleep(pause) result_res = await lint_result(create_res.lint_id, profile, session) status = result_res.status @@ -23,7 +23,7 @@ async def lint(body: str, is_html: bool = False, profile: Optional[str] = None) if pause < 16: pause *= 2 - if status == Lint.STATUS_FAILED: + if status == Lint.STATUS_FAILED.value: raise LintFailed return LintResult(status=status, messages=messages, updated=result_res.updated) diff --git a/shodo/lint.py b/shodo/lint.py index 6920914..aaaa007 100644 --- a/shodo/lint.py +++ b/shodo/lint.py @@ -1,4 +1,5 @@ import time +from enum import Enum from dataclasses import asdict, dataclass from datetime import datetime from typing import Any, Dict, List, Optional @@ -64,46 +65,17 @@ class LintFailed(Exception): pass -class Lint: +class Lint(Enum): STATUS_PROCESSING = "processing" STATUS_FAILED = "failed" - def __init__(self, body, lint_id, profile): - self.body = body - self.lint_id = lint_id - self.body = None - self.status = self.STATUS_PROCESSING - self.messages = [] - self.profile = profile - - def results(self): - while self.status == self.STATUS_PROCESSING: - time.sleep(0.5) - res = lint_result(self.lint_id, self.profile) - self.status = res.status - msgs = [Message.load(m) for m in res.messages] - self.messages = sorted(msgs, key=lambda m: (m.from_.line, m.from_.ch)) - - if self.status == self.STATUS_FAILED: - raise LintFailed - - return self.messages - - def __repr__(self): - return f"Lint({self.lint_id})" - - @classmethod - def start(cls, body: str, is_html: bool = False, profile: Optional[str] = None): - res = lint_create(body, is_html, profile) - return cls(body, res.lint_id, profile) - def lint(body: str, is_html: bool = False, profile: Optional[str] = None) -> LintResult: create_res = lint_create(body, is_html, profile) - status = Lint.STATUS_PROCESSING + status = Lint.STATUS_PROCESSING.value pause = 0.25 - while status == Lint.STATUS_PROCESSING: + while status == Lint.STATUS_PROCESSING.value: time.sleep(pause) result_res = lint_result(create_res.lint_id, profile) status = result_res.status @@ -113,7 +85,7 @@ def lint(body: str, is_html: bool = False, profile: Optional[str] = None) -> Lin if pause < 16: pause *= 2 - if status == Lint.STATUS_FAILED: + if status == Lint.STATUS_FAILED.value: raise LintFailed return LintResult(status=status, messages=messages, updated=result_res.updated) diff --git a/shodo/main.py b/shodo/main.py index 7e97e8d..621e882 100644 --- a/shodo/main.py +++ b/shodo/main.py @@ -10,7 +10,7 @@ from shodo.api import download_image, list_post_files from shodo.conf import UnableLocateCredentialsError, save_credentials -from shodo.lint import Lint +from shodo.lint import lint as shodo_lint class ClickCatchExceptions(click.Group): @@ -72,20 +72,20 @@ def lint(filename, html, output, profile): if not body: return - linting = Lint.start(body, is_html=html, profile=profile) click.echo("Linting...", err=True) + result = shodo_lint(body, is_html=html, profile=profile) if output == "json": click.echo( json.dumps( - [msg.asdict() for msg in linting.results()], + [msg.asdict() for msg in result.messages], ensure_ascii=False, indent=2, ) ) return - for message in linting.results(): + for message in result.messages: if message.score < 0.5: continue color = "red" if message.severity == message.ERROR else "yellow" @@ -93,11 +93,7 @@ def lint(filename, html, output, profile): body[message.index - 10 : message.index] + click.style( body[message.index : message.index_to] - + ( - f"(→ {message.after or 'トル'})" - if message.after is not None - else "" - ), + + (f"(→ {message.after or 'トル'})" if message.after is not None else ""), color, ) + body[message.index_to : message.index_to + 10] @@ -109,7 +105,7 @@ def lint(filename, html, output, profile): click.echo(" ", nl=False) click.echo(body_highlight) - if linting.messages: + if result.messages: sys.exit(1) diff --git a/tests/test_main.py b/tests/test_main.py index 4fe81c7..4492ef7 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,11 +1,11 @@ -import uuid +from datetime import datetime from pathlib import Path import pytest from click.testing import CliRunner from shodo.conf import UnableLocateCredentialsError -from shodo.lint import Lint, Message +from shodo.lint import LintResult, Message from shodo.main import cli @@ -65,40 +65,42 @@ def test_with_profile(self, mocker, runner, profile): class TestLint: def test_no_profile(self, mocker, runner, filename): body = filename.read_text(encoding="utf-8") - linting = Lint(body, str(uuid.uuid4()), None) - m_lint = mocker.patch("shodo.main.Lint.start", return_value=linting) - stub_results = [ - Message.load(data) - for data in [ - { - "code": "variants:fuzzy", - "message": "表記ゆれがあります", - "severity": "error", - "to": { - "line": 0, - "ch": 12, + result = LintResult( + "done", + [ + Message.load(data) + for data in [ + { + "code": "variants:fuzzy", + "message": "表記ゆれがあります", + "severity": "error", + "to": { + "line": 0, + "ch": 12, + }, + "index": 2, + "index_to": 12, + "score": 0.8888888888888888, + "before": "Shodo AI校正", + "after": "Shodo AI校正", + "operation": "replace", + "meta": { + "description": "", + }, + "from": { + "line": 0, + "ch": 2, + }, }, - "index": 2, - "index_to": 12, - "score": 0.8888888888888888, - "before": "Shodo AI校正", - "after": "Shodo AI校正", - "operation": "replace", - "meta": { - "description": "", - }, - "from": { - "line": 0, - "ch": 2, - }, - }, - ] - ] - mocker.patch.object(Lint, "results", return_value=stub_results) + ] + ], + datetime.now(), + ) + m_lint = mocker.patch("shodo.main.shodo_lint", return_value=result) actual = runner.invoke(cli, args=["lint", str(filename)], color=True) - assert actual.exit_code == 0 + assert actual.exit_code == 1 assert ( actual.output == "Linting...\n1:3 表記ゆれがあります\n \x1b[31mShodo AI校正(→ Shodo AI校正)\x1b[0m 飛行機の欠便があ\n" @@ -109,9 +111,9 @@ def test_no_profile(self, mocker, runner, filename): @pytest.mark.parametrize("profile", ["default", "tests"], ids=["default", "tests"]) def test_with_profile(self, mocker, runner, filename, profile): body = filename.read_text(encoding="utf-8") - linting = Lint(body, str(uuid.uuid4()), profile) - m_lint = mocker.patch("shodo.main.Lint.start", return_value=linting) - mocker.patch.object(Lint, "results", return_value=[]) + m_lint = mocker.patch( + "shodo.main.shodo_lint", return_value=LintResult("done", [], datetime.now()) + ) actual = runner.invoke(cli, ["lint", str(filename), "--profile", profile]) @@ -123,7 +125,7 @@ def test_with_profile(self, mocker, runner, filename, profile): def test_no_existing_default_profile(self, mocker, filename, runner): body = filename.read_text(encoding="utf-8") m_lint = mocker.patch( - "shodo.main.Lint.start", + "shodo.main.shodo_lint", side_effect=UnableLocateCredentialsError( "Use 'shodo login' to save credentials before running." ), @@ -139,7 +141,7 @@ def test_no_existing_default_profile(self, mocker, filename, runner): def test_not_found_profile(self, mocker, filename, runner): body = filename.read_text(encoding="utf-8") m_lint = mocker.patch( - "shodo.main.Lint.start", + "shodo.main.shodo_lint", side_effect=UnableLocateCredentialsError( "The config profile (tests) could not be found." ), From 43f2b41748681e6967c3d4a6f4c7ac2e2332f11f Mon Sep 17 00:00:00 2001 From: Hiroki KIYOHARA Date: Fri, 15 Nov 2024 12:00:14 +0900 Subject: [PATCH 2/4] Fixed Enum name and type cheking --- shodo/aio/lint.py | 8 ++++---- shodo/lint.py | 13 +++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/shodo/aio/lint.py b/shodo/aio/lint.py index c9f7733..8981c38 100644 --- a/shodo/aio/lint.py +++ b/shodo/aio/lint.py @@ -4,16 +4,16 @@ import aiohttp from shodo.aio.api import lint_create, lint_result -from shodo.lint import Lint, LintFailed, LintResult, Message +from shodo.lint import LintFailed, LintResult, LintStatus, Message async def lint(body: str, is_html: bool = False, profile: Optional[str] = None) -> LintResult: async with aiohttp.ClientSession() as session: create_res = await lint_create(body, is_html, profile, session) - status = Lint.STATUS_PROCESSING.value + status = LintStatus.PROCESSING.value pause = 0.25 - while status == Lint.STATUS_PROCESSING.value: + while status == LintStatus.PROCESSING: await asyncio.sleep(pause) result_res = await lint_result(create_res.lint_id, profile, session) status = result_res.status @@ -23,7 +23,7 @@ async def lint(body: str, is_html: bool = False, profile: Optional[str] = None) if pause < 16: pause *= 2 - if status == Lint.STATUS_FAILED.value: + if status == LintStatus.FAILED: raise LintFailed return LintResult(status=status, messages=messages, updated=result_res.updated) diff --git a/shodo/lint.py b/shodo/lint.py index aaaa007..001df6d 100644 --- a/shodo/lint.py +++ b/shodo/lint.py @@ -65,17 +65,18 @@ class LintFailed(Exception): pass -class Lint(Enum): - STATUS_PROCESSING = "processing" - STATUS_FAILED = "failed" +class LintStatus(Enum): + PROCESSING = "processing" + FAILED = "failed" + DONE = "done" def lint(body: str, is_html: bool = False, profile: Optional[str] = None) -> LintResult: create_res = lint_create(body, is_html, profile) - status = Lint.STATUS_PROCESSING.value + status = LintStatus.PROCESSING.value pause = 0.25 - while status == Lint.STATUS_PROCESSING.value: + while status == LintStatus.PROCESSING: time.sleep(pause) result_res = lint_result(create_res.lint_id, profile) status = result_res.status @@ -85,7 +86,7 @@ def lint(body: str, is_html: bool = False, profile: Optional[str] = None) -> Lin if pause < 16: pause *= 2 - if status == Lint.STATUS_FAILED.value: + if status == LintStatus.FAILED: raise LintFailed return LintResult(status=status, messages=messages, updated=result_res.updated) From b263e147b080e5bc3e40a55c2d1a2b69cdb6e88d Mon Sep 17 00:00:00 2001 From: Hiroki KIYOHARA Date: Fri, 15 Nov 2024 14:26:57 +0900 Subject: [PATCH 3/4] Bug fixed and added tests for lint.py --- setup.py | 3 +++ shodo/aio/lint.py | 8 +++---- shodo/lint.py | 10 ++++----- tests/conftest.py | 7 ++++++ tests/test_aio/__init__.py | 0 tests/test_aio/test_lint.py | 43 +++++++++++++++++++++++++++++++++++++ tests/test_lint.py | 40 ++++++++++++++++++++++++++++++++++ 7 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_aio/__init__.py create mode 100644 tests/test_aio/test_lint.py create mode 100644 tests/test_lint.py diff --git a/setup.py b/setup.py index 61199ac..626daf8 100644 --- a/setup.py +++ b/setup.py @@ -25,8 +25,11 @@ "aiohttp", ], "dev": [ + "aioresponses", "pytest", + "pytest-asyncio", "pytest-mock", + "pytest-responses", "mypy", "types-requests", ], diff --git a/shodo/aio/lint.py b/shodo/aio/lint.py index 8981c38..671a6b6 100644 --- a/shodo/aio/lint.py +++ b/shodo/aio/lint.py @@ -7,13 +7,13 @@ from shodo.lint import LintFailed, LintResult, LintStatus, Message -async def lint(body: str, is_html: bool = False, profile: Optional[str] = None) -> LintResult: +async def lint(body: str, is_html: bool = False, profile: Optional[str] = None, _initial_pause: float = 0.25) -> LintResult: async with aiohttp.ClientSession() as session: create_res = await lint_create(body, is_html, profile, session) status = LintStatus.PROCESSING.value - pause = 0.25 - while status == LintStatus.PROCESSING: + pause = _initial_pause + while status == LintStatus.PROCESSING.value: await asyncio.sleep(pause) result_res = await lint_result(create_res.lint_id, profile, session) status = result_res.status @@ -23,7 +23,7 @@ async def lint(body: str, is_html: bool = False, profile: Optional[str] = None) if pause < 16: pause *= 2 - if status == LintStatus.FAILED: + if status == LintStatus.FAILED.value: raise LintFailed return LintResult(status=status, messages=messages, updated=result_res.updated) diff --git a/shodo/lint.py b/shodo/lint.py index 001df6d..453a554 100644 --- a/shodo/lint.py +++ b/shodo/lint.py @@ -1,7 +1,7 @@ import time -from enum import Enum from dataclasses import asdict, dataclass from datetime import datetime +from enum import Enum from typing import Any, Dict, List, Optional from shodo.api import lint_create, lint_result @@ -71,12 +71,12 @@ class LintStatus(Enum): DONE = "done" -def lint(body: str, is_html: bool = False, profile: Optional[str] = None) -> LintResult: +def lint(body: str, is_html: bool = False, profile: Optional[str] = None, _initial_pause: float=0.25) -> LintResult: create_res = lint_create(body, is_html, profile) status = LintStatus.PROCESSING.value - pause = 0.25 - while status == LintStatus.PROCESSING: + pause = _initial_pause + while status == LintStatus.PROCESSING.value: time.sleep(pause) result_res = lint_result(create_res.lint_id, profile) status = result_res.status @@ -86,7 +86,7 @@ def lint(body: str, is_html: bool = False, profile: Optional[str] = None) -> Lin if pause < 16: pause *= 2 - if status == LintStatus.FAILED: + if status == LintStatus.FAILED.value: raise LintFailed return LintResult(status=status, messages=messages, updated=result_res.updated) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..99c8ba9 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +import pytest + + +@pytest.fixture +def credential(monkeypatch): + monkeypatch.setenv("SHODO_API_ROOT", "https://api.shodo.ink/@shodo/shodo/") + monkeypatch.setenv("SHODO_API_TOKEN", "test-token") diff --git a/tests/test_aio/__init__.py b/tests/test_aio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_aio/test_lint.py b/tests/test_aio/test_lint.py new file mode 100644 index 0000000..fc4bff6 --- /dev/null +++ b/tests/test_aio/test_lint.py @@ -0,0 +1,43 @@ +from datetime import datetime +from aioresponses import aioresponses + +import pytest +from shodo.aio.lint import lint + + +@pytest.mark.asyncio +async def test_lint(credential): + with aioresponses() as m: + m.post( + "https://api.shodo.ink/@shodo/shodo/lint/", + payload={ + "lint_id": "spam-spam-spam", + "monthly_amount": 100, + "current_usage": 10, + "len_body": 10, + "len_used": 10, + }, + ) + m.get( + "https://api.shodo.ink/@shodo/shodo/lint/spam-spam-spam/", + payload={"status": "done", "updated": 1_700_000_000, "messages": []}, + ) + actual = await lint("body", is_html=False) + + assert actual.status == "done" + assert actual.messages == [] + assert actual.updated == datetime(2023, 11, 15, 7, 13, 20) + m.assert_called_with( + "https://api.shodo.ink/@shodo/shodo/lint/", + "post", + json={ + "body": "body", + "type": "text", + }, + headers={"Authorization": "Bearer test-token"}, + ) + m.assert_called_with( + "https://api.shodo.ink/@shodo/shodo/lint/spam-spam-spam/", + "get", + headers={"Authorization": "Bearer test-token"}, + ) diff --git a/tests/test_lint.py b/tests/test_lint.py new file mode 100644 index 0000000..6b29fe6 --- /dev/null +++ b/tests/test_lint.py @@ -0,0 +1,40 @@ +import json +from datetime import datetime + +from shodo.lint import lint + + +class TestLint: + def test_lint(self, credential, responses): + responses.add( + "POST", + "https://api.shodo.ink/@shodo/shodo/lint/", + json={ + "lint_id": "spam-spam-spam", + "monthly_amount": 100, + "current_usage": 10, + "len_body": 10, + "len_used": 10, + }, + ) + responses.add( + "GET", + "https://api.shodo.ink/@shodo/shodo/lint/spam-spam-spam/", + json={ + "status": "done", + "messages": [], + "updated": 1_700_000_000, + }, + ) + + actual = lint("これはテストです", is_html=False, profile=None, _initial_pause=0) + + assert actual.status == "done" + assert actual.messages == [] + assert actual.updated == datetime(2023, 11, 15, 7, 13, 20) + + assert len(responses.calls) == 2 + assert json.loads(responses.calls[0].request.body.decode("utf-8")) == { + "body": "これはテストです", + "type": "text", + } From a0d83cf4cab1e16b7b74d5eb3e21154580b65c0b Mon Sep 17 00:00:00 2001 From: Hiroki KIYOHARA Date: Fri, 15 Nov 2024 14:55:52 +0900 Subject: [PATCH 4/4] Make LintResult timezone aware --- shodo/api.py | 11 +++++++++-- tests/test_aio/test_lint.py | 4 ++-- tests/test_lint.py | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/shodo/api.py b/shodo/api.py index eaa9fd3..86a78eb 100644 --- a/shodo/api.py +++ b/shodo/api.py @@ -1,6 +1,6 @@ import time from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, Generator, List, Optional @@ -8,6 +8,13 @@ from shodo.conf import conf +try: + from zoneinfo import ZoneInfo + JST = ZoneInfo("Asia/Tokyo") +except ImportError: + from datetime import timedelta, timezone + JST = timezone(timedelta(hours=+9), "JST") + def api_path(path, profile) -> str: return conf(profile).api_root.rstrip("/") + "/" + path.strip("/") + "/" @@ -37,7 +44,7 @@ class LintResultResponse: def __post_init__(self) -> None: if isinstance(self.updated, int): - self.updated = datetime.fromtimestamp(self.updated) + self.updated = datetime.fromtimestamp(self.updated, JST) def lint_create( diff --git a/tests/test_aio/test_lint.py b/tests/test_aio/test_lint.py index fc4bff6..b018bcb 100644 --- a/tests/test_aio/test_lint.py +++ b/tests/test_aio/test_lint.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from aioresponses import aioresponses import pytest @@ -26,7 +26,7 @@ async def test_lint(credential): assert actual.status == "done" assert actual.messages == [] - assert actual.updated == datetime(2023, 11, 15, 7, 13, 20) + assert actual.updated.timetuple()[:6] == (2023, 11, 15, 7, 13, 20) m.assert_called_with( "https://api.shodo.ink/@shodo/shodo/lint/", "post", diff --git a/tests/test_lint.py b/tests/test_lint.py index 6b29fe6..642a614 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -1,5 +1,5 @@ import json -from datetime import datetime +from datetime import datetime, timezone from shodo.lint import lint @@ -31,7 +31,7 @@ def test_lint(self, credential, responses): assert actual.status == "done" assert actual.messages == [] - assert actual.updated == datetime(2023, 11, 15, 7, 13, 20) + assert actual.updated.timetuple()[:6] == (2023, 11, 15, 7, 13, 20) assert len(responses.calls) == 2 assert json.loads(responses.calls[0].request.body.decode("utf-8")) == {