Skip to content

Commit 31cd1c8

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

12 files changed

Lines changed: 77 additions & 39 deletions

File tree

.github/workflows/build-test.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,15 @@ 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+
WHEEL_FILE=$(ls dist/*.whl | head -1)
44+
echo "Installing wheel: `$WHEEL_FILE` with `cli` extra."
45+
uv pip install "${WHEEL_FILE}[cli]"
4046
# Test that the CLI is available and --version works
4147
uv run twyn --version
4248

.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

dependencies/scripts/download_packages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def download(ecosystem: str) -> None:
6161
):
6262
with attempt, httpx.Client(timeout=30) as client:
6363
logger.info("Attempting to download %s packages. Attempt #%d.", ecosystem, attempt.num)
64-
response = client.get(ECOSYSTEMS[ecosystem]["url"]) # type: ignore[arg-type]
64+
response = client.get(ECOSYSTEMS[ecosystem]["url"])
6565
response.raise_for_status()
6666

6767
fpath = Path("dependencies") / f"{ecosystem}.json"

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: 16 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,
@@ -88,21 +87,31 @@ def check_dependencies(
8887
normalized_dependencies = top_package_reference.normalize_packages(dependencies_to_check)
8988

9089
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:
90+
91+
for dependency in _get_dependencies_list(normalized_dependencies, show_progress_bar):
9592
if dependency in normalized_allowlist_packages:
9693
logger.info("Dependency %s is in the allowlist", dependency)
9794
continue
9895

9996
logger.info("Analyzing %s", dependency)
10097
if dependency not in trusted_packages and (typosquat_results := trusted_packages.get_typosquat(dependency)):
10198
typos_list.errors.append(typosquat_results)
102-
10399
return typos_list
104100

105101

102+
def _get_dependencies_list(normalized_dependencies: set[str], show_progress_bar: bool) -> Iterable[str]:
103+
try:
104+
from rich.progress import track # noqa: PLC0415
105+
106+
return (
107+
track(normalized_dependencies, description="Processing...")
108+
if show_progress_bar
109+
else normalized_dependencies
110+
)
111+
except ImportError:
112+
return normalized_dependencies
113+
114+
106115
def _get_selector_method(selector_method: str) -> SelectorMethod:
107116
if selector_method not in SELECTOR_METHOD_MAPPING:
108117
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

0 commit comments

Comments
 (0)