From 52f3fd6f166746eff0821f902677dbea958bdd99 Mon Sep 17 00:00:00 2001 From: Roy Reznik Date: Sat, 3 Feb 2024 14:10:31 +0200 Subject: [PATCH] Adding typer --- .github/workflows/tests.yml | 56 +++++++++++++++++++++++++++++++++++++ Makefile | 3 +- poetry.lock | 23 ++++++++++++++- pyproject.toml | 3 +- rexi/cli.py | 21 ++++++++++++++ rexi/rexi.py | 29 ++++--------------- tests/test_cli.py | 26 +++++++++++++++++ tests/test_logic.py | 5 ++-- tests/test_ui.py | 37 ++++++++++++++---------- 9 files changed, 160 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 rexi/cli.py create mode 100644 tests/test_cli.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3534e65 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,56 @@ +name: Test +on: + push: + branches: + - master + pull_request: + paths: + - rexi/** + - tests/** + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - uses: snok/install-poetry@v1 + - uses: actions/cache@v3 + id: cached-poetry-dependencies + with: + path: .venv + key: venv-Linux-3.11-${{ hashFiles('**/pyproject.toml') }} + - name: install deps + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: make install + - name: lint + run: make lint + + test: + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - uses: snok/install-poetry@v1 + - uses: actions/cache@v3 + id: cached-poetry-dependencies + with: + path: .venv + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} + - name: install deps + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: rm -rf poetry.lock && make install + - name: test + run: make test + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/Makefile b/Makefile index da825ce..141a0a2 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,8 @@ publish: build poetry publish format: - poetry run black rexi/ tests/ + poetry run ruff --fix rexi/ tests/ + poetry run ruff format rexi/ tests/ lint: poetry run ruff rexi/ tests/ diff --git a/poetry.lock b/poetry.lock index b247561..6eaaaea 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1388,6 +1388,27 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] +[[package]] +name = "typer" +version = "0.9.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.6" +files = [ + {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, + {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, +] + +[package.dependencies] +click = ">=7.1.1,<9.0.0" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] + [[package]] name = "types-colorama" version = "0.4.15.20240106" @@ -1541,4 +1562,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "43b9503cb65976868dc6c2b86dd994701b80d27d4373f188583493db3013045d" +content-hash = "8d637776c787baf7d162e7c893495829d1d60761b38dee32fa770c802b45d89e" diff --git a/pyproject.toml b/pyproject.toml index eab208a..6fd6d05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,12 +15,13 @@ classifiers = [ ] [tool.poetry.scripts] -rexi = "rexi.rexi:main" +rexi = "rexi.cli:app" [tool.poetry.dependencies] python = "^3.10" textual = "^0.48.1" colorama = "^0.4.6" +typer = "^0.9.0" [tool.poetry.group.dev.dependencies] diff --git a/rexi/cli.py b/rexi/cli.py new file mode 100644 index 0000000..934bfc7 --- /dev/null +++ b/rexi/cli.py @@ -0,0 +1,21 @@ +import os +import sys + +import typer + +from .rexi import RexiApp + +app = typer.Typer() + + +# noinspection SpellCheckingInspection +@app.command("rexi") +def rexi_cli() -> None: + stdin = sys.stdin.read() + try: + os.close(sys.stdin.fileno()) + except OSError: + pass + sys.stdin = open("/dev/tty", "rb") # type: ignore[assignment] + app: RexiApp[int] = RexiApp(stdin) + app.run() diff --git a/rexi/rexi.py b/rexi/rexi.py index 08a9d6f..b90617d 100644 --- a/rexi/rexi.py +++ b/rexi/rexi.py @@ -1,10 +1,8 @@ import dataclasses -import os import re -import sys from typing import cast, Match, Iterable, Optional -from colorama import Fore, Style +from colorama import Fore from textual import on from textual.app import App, ComposeResult, ReturnType from textual.containers import ScrollableContainer, Horizontal @@ -45,7 +43,10 @@ def compose(self) -> ComposeResult: with Horizontal(id="inputs"): yield Input(placeholder="Enter regex pattern") yield Select( - zip(self.regex_modes, self.regex_modes), id="select", allow_blank=False, value=self.regex_current_mode + zip(self.regex_modes, self.regex_modes), + id="select", + allow_blank=False, + value=self.regex_current_mode, ) with ScrollableContainer(id="result"): @@ -132,12 +133,7 @@ def combine_matches_groups( @staticmethod def _combine_groups(match: Match[str]) -> list["GroupMatch"]: - groups = [ - GroupMatch([index], group, start, end, is_first=True) - for index, (group, (start, end)) in enumerate( - [[match.group(0), match.regs[0]]] - ) - ] + groups = [GroupMatch([0], match.group(0), *match.regs[0], is_first=True)] groups += [ GroupMatch([index], group, start, end) for index, (group, (start, end)) in enumerate( @@ -151,16 +147,3 @@ def _combine_groups(match: Match[str]) -> list["GroupMatch"]: if group_match in groups: groups[groups.index(group_match)].keys.append(group_name) return groups - - -def main() -> None: - stdin = sys.stdin.read() - os.close(sys.stdin.fileno()) - sys.stdin = open("/dev/tty", "rb") # type: ignore[assignment] - - app: RexiApp[int] = RexiApp(stdin) - app.run() - - -if __name__ == "__main__": - main() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..bc196dc --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,26 @@ +from io import BytesIO +from unittest.mock import Mock + +from _pytest.monkeypatch import MonkeyPatch +from typer.testing import CliRunner +from rexi.cli import app + + +def test_app(monkeypatch: MonkeyPatch) -> None: + """ + Couldn't find a better way to test the CLI without patching everything :( + """ + runner = CliRunner() + text = b"This iS! aTe xt2 F0r T3sT!ng" + a = BytesIO(text) + class_mock = Mock() + instance_mock = Mock() + open_mock = Mock() + with monkeypatch.context(): + class_mock.return_value = instance_mock + monkeypatch.setattr("rexi.cli.RexiApp", class_mock) + monkeypatch.setattr("builtins.open", open_mock) + runner.invoke(app, input=a) + open_mock.assert_called_once_with("/dev/tty", "rb") + class_mock.assert_called_once_with(text.decode()) + instance_mock.run.assert_called_once() diff --git a/tests/test_logic.py b/tests/test_logic.py index e8d7106..93d41ad 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -17,7 +17,7 @@ ) def test_group_match_equals( group_one: GroupMatch, group_two: GroupMatch, expected_equals: bool -): +) -> None: assert (group_one == group_two) == expected_equals @@ -42,7 +42,8 @@ def test_group_match_equals( ], ], ) -def test_combine_groups(pattern, content, expected_groups): +def test_combine_groups(pattern: str, content: str, expected_groups: list[GroupMatch]) -> None: matches = re.match(pattern, content) + assert matches # sanity result = RexiApp._combine_groups(matches) assert result == expected_groups, result[0].end diff --git a/tests/test_ui.py b/tests/test_ui.py index c6badd4..061e8ac 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -2,36 +2,43 @@ import pytest from colorama import Fore -from textual.pilot import Pilot from textual.widgets import Static from rexi.rexi import RexiApp, UNDERLINE, RESET_UNDERLINE + @pytest.mark.parametrize( ("start_mode", "pattern", "expected_output"), [ - ["match", ".*(aTe).*", f"{UNDERLINE}This iS! {Fore.RED}aTe{Fore.RESET} xt2 F0r T3sT!ng{RESET_UNDERLINE}"], - ["match", ".*(aTe.*)", f"{UNDERLINE}This iS! {Fore.RED}aTe xt2 F0r T3sT!ng{Fore.RESET}{RESET_UNDERLINE}"], - ["finditer", "(aTe)", f"This iS! {UNDERLINE}{Fore.RED}aTe{Fore.RESET}{RESET_UNDERLINE} xt2 F0r T3sT!ng"] - ] + [ + "match", + ".*(aTe).*", + f"{UNDERLINE}This iS! {Fore.RED}aTe{Fore.RESET} xt2 F0r T3sT!ng{RESET_UNDERLINE}", + ], + [ + "match", + ".*(aTe.*)", + f"{UNDERLINE}This iS! {Fore.RED}aTe xt2 F0r T3sT!ng{Fore.RESET}{RESET_UNDERLINE}", + ], + [ + "finditer", + "(aTe)", + f"This iS! {UNDERLINE}{Fore.RED}aTe{Fore.RESET}{RESET_UNDERLINE} xt2 F0r T3sT!ng", + ], + ], ) -async def test_input_box(start_mode, pattern, expected_output): - app = RexiApp("This iS! aTe xt2 F0r T3sT!ng", start_mode=start_mode) +async def test_input_box(start_mode: str, pattern: str, expected_output: str) -> None: + app: RexiApp[int] = RexiApp("This iS! aTe xt2 F0r T3sT!ng", start_mode=start_mode) async with app.run_test() as pilot: - pilot: Pilot await pilot.click("Input") await pilot.press(*list(pattern)) result = str(cast(Static, app.query_one("#output")).renderable) - assert ( - result - == expected_output - ) + assert result == expected_output -async def test_switch_modes(): - app = RexiApp("This iS! aTe xt2 F0r T3sT!ng") +async def test_switch_modes() -> None: + app: RexiApp[int] = RexiApp("This iS! aTe xt2 F0r T3sT!ng") async with app.run_test() as pilot: - pilot: Pilot assert app.regex_current_mode == "finditer" await pilot.click("SelectCurrent") await pilot.click("SelectOverlay", offset=(2, 2))