Skip to content

Commit dc8858e

Browse files
committed
feat: create cli extra group
BREAKING CHANGE
1 parent d0b5857 commit dc8858e

11 files changed

Lines changed: 75 additions & 38 deletions

File tree

.github/workflows/build-test.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ jobs:
3434
WHEEL_FILE=$(ls dist/*.whl | head -1)
3535
echo "Installing wheel: $WHEEL_FILE"
3636
uv pip install "$WHEEL_FILE"
37+
38+
- name: Test twyn as a library
39+
run: uv run python -c "import twyn; twyn.check_dependencies"
3740

38-
- name: Test --version flag
41+
- name: Test twyn as a cli tool
3942
run: |
43+
uv pip install "$WHEEL_FILE[cli]"
4044
# Test that the CLI is available and --version works
4145
uv run twyn --version
4246

.github/workflows/security.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
4040

4141
- name: Install the project
42-
run: uv sync --locked
42+
run: uv sync --locked --extra cli
4343

4444
- name: Run Twyn against our dependencies
4545
run: |

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
2525

2626
- name: Install the dependencies
27-
run: uv sync --locked --group dev --python ${{ matrix.python-version }}
27+
run: uv sync --locked --group dev --all-extras --python ${{ matrix.python-version }}
2828

2929
- name: Run tests
3030
run: uv run pytest tests

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ help:
1616
venv:
1717
@if ! {{ venv-exists }}; \
1818
then \
19-
uv sync --frozen --all-groups; \
19+
uv sync --frozen --all-extras --all-groups; \
2020
fi
2121

2222
# Cleans all artifacts generated while running this project, including the virtualenv.

pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ maintainers = [
1212
requires-python = "<4,>=3.9"
1313
dependencies = [
1414
"requests<3.0.0,>=2.32.4",
15-
"click<9.0.0,>=8.1.8",
16-
"rich<15.0.0,>=14.0.0",
1715
"rapidfuzz<4.0.0,>=2.13.7",
1816
"pyparsing<4.0.0,>=3.2.3",
1917
"tomlkit<0.14.0,>=0.11.6",
@@ -26,6 +24,13 @@ description = "Security tool against dependency typosquatting attacks"
2624
readme = "README.md"
2725
dynamic = ["version"]
2826

27+
[project.optional-dependencies]
28+
cli = [
29+
"click<9.0.0,>=8.1.8",
30+
"rich<15.0.0,>=14.0.0",
31+
]
32+
33+
2934
[tool.hatch.version]
3035
path = "VERSION"
3136
pattern = "v(?P<version>[^\\s]+)"

src/twyn/base/exceptions.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import logging
22
from typing import IO, Any, Optional
33

4-
import click
5-
64
logger = logging.getLogger("twyn")
75

86

@@ -19,14 +17,20 @@ def __init__(self, message: str = "") -> None:
1917
super().__init__(message or self.message)
2018

2119

22-
class CliError(click.ClickException):
23-
"""Error that will populate application errors to stdout. It does not inherit from `TwynError`."""
20+
try:
21+
import click
2422

25-
message = "CLI error"
23+
class CliError(click.ClickException):
24+
"""Error that will populate application errors to stdout. It does not inherit from `TwynError`."""
2625

27-
def __init__(self, message: str = "") -> None:
28-
super().__init__(message)
26+
message = "CLI error"
27+
28+
def __init__(self, message: str = "") -> None:
29+
super().__init__(message)
30+
31+
def show(self, file: Optional[IO[Any]] = None) -> None:
32+
logger.debug(self.format_message(), exc_info=True)
33+
logger.error(self.format_message(), exc_info=False)
2934

30-
def show(self, file: Optional[IO[Any]] = None) -> None:
31-
logger.debug(self.format_message(), exc_info=True)
32-
logger.error(self.format_message(), exc_info=False)
35+
except ModuleNotFoundError:
36+
pass

src/twyn/cli.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,38 @@
22
import sys
33
from typing import Optional
44

5-
import click
6-
from rich.console import Console
7-
from rich.logging import RichHandler
8-
95
from twyn.__version__ import __version__
106
from twyn.base.constants import (
117
DEFAULT_PROJECT_TOML_FILE,
128
DEPENDENCY_FILE_MAPPING,
139
SELECTOR_METHOD_MAPPING,
1410
)
15-
from twyn.base.exceptions import CliError, TwynError
11+
from twyn.base.exceptions import TwynError
1612
from twyn.config.config_handler import ConfigHandler
1713
from twyn.file_handler.file_handler import FileHandler
1814
from twyn.main import check_dependencies
1915
from twyn.trusted_packages.cache_handler import CacheHandler
2016
from twyn.trusted_packages.constants import CACHE_DIR
2117

18+
try:
19+
import click
20+
from rich.console import Console
21+
from rich.logging import RichHandler
22+
23+
from twyn.base.exceptions import CliError
24+
except ImportError:
25+
print("Could not run twyn as a cli tool, some dependencies are missing! Run `pip install twyn[cli]`.")
26+
import sys
27+
28+
sys.exit(1)
29+
30+
logger = logging.getLogger("twyn")
31+
2232
logging.basicConfig(
2333
format="%(message)s",
2434
datefmt="[%X]",
2535
handlers=[RichHandler(rich_tracebacks=True, show_path=False, console=Console(stderr=True))],
2636
)
27-
logger = logging.getLogger("twyn")
2837

2938

3039
@click.group()

src/twyn/main.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import logging
2+
from collections.abc import Iterable
23
from typing import Optional, Union
34

4-
from rich.progress import track
5-
65
from twyn.base.constants import (
76
SELECTOR_METHOD_MAPPING,
87
PackageEcosystems,
@@ -59,6 +58,7 @@ def check_dependencies(
5958
Returns:
6059
TyposquatCheckResultList: A list of results indicating which dependencies, if any, are suspected typosquats.
6160
"""
61+
6262
config = _get_config(
6363
load_config_from_file=load_config_from_file,
6464
config_file=config_file,
@@ -88,21 +88,31 @@ def check_dependencies(
8888
normalized_dependencies = top_package_reference.normalize_packages(dependencies_to_check)
8989

9090
typos_list = TyposquatCheckResultList()
91-
dependencies_list = (
92-
track(normalized_dependencies, description="Processing...") if show_progress_bar else normalized_dependencies
93-
)
94-
for dependency in dependencies_list:
91+
92+
for dependency in _get_dependencies_list(normalized_dependencies, show_progress_bar):
9593
if dependency in normalized_allowlist_packages:
9694
logger.info("Dependency %s is in the allowlist", dependency)
9795
continue
9896

9997
logger.info("Analyzing %s", dependency)
10098
if dependency not in trusted_packages and (typosquat_results := trusted_packages.get_typosquat(dependency)):
10199
typos_list.errors.append(typosquat_results)
102-
103100
return typos_list
104101

105102

103+
def _get_dependencies_list(normalized_dependencies: set[str], show_progress_bar: bool) -> Iterable[str]:
104+
try:
105+
from rich.progress import track # noqa: PLC0415
106+
107+
return (
108+
track(normalized_dependencies, description="Processing...")
109+
if show_progress_bar
110+
else normalized_dependencies
111+
)
112+
except ImportError:
113+
return normalized_dependencies
114+
115+
106116
def _get_selector_method(selector_method: str) -> SelectorMethod:
107117
if selector_method not in SELECTOR_METHOD_MAPPING:
108118
InvalidSelectorMethodError("Invalid selector method")

tests/main/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
@pytest.fixture(scope="module")
99
def disable_track() -> Generator[None, Any, None]:
1010
"""Disables the track UI for running tests."""
11-
with patch("twyn.main.track") as m_track:
11+
with patch("rich.progress.track") as m_track:
1212
m_track.side_effect = lambda iterable, description: iterable
1313
yield

tests/main/test_main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ def test_track_is_disabled_by_default_when_used_as_package(
312312
package_ecosystem=None,
313313
)
314314
mock_get_packages.return_value = {"requests"}
315-
with patch("twyn.main.track") as m_track:
315+
with patch("rich.progress.track") as m_track:
316316
check_dependencies()
317317
assert m_track.call_count == 0
318318

@@ -328,7 +328,7 @@ def test_track_is_shown_when_enabled(self, mock_config: Mock, mock_get_packages:
328328
package_ecosystem=None,
329329
)
330330
mock_get_packages.return_value = {"requests"}
331-
with patch("twyn.main.track") as m_track:
331+
with patch("rich.progress.track") as m_track:
332332
check_dependencies(show_progress_bar=True)
333333
assert m_track.call_count == 1
334334

0 commit comments

Comments
 (0)