Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions .github/actions/python-package-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,19 @@ runs:
id: build
shell: bash
run: |
uv build
uv build --python ${{ inputs.python-version }}

# Set outputs
WHEEL_FILE=$(ls dist/*.whl | head -1)
echo "wheel-file=$WHEEL_FILE" >> $GITHUB_OUTPUT
echo "dist-path=dist" >> $GITHUB_OUTPUT
echo "Built wheel: $WHEEL_FILE"

- name: Install the built wheel
shell: bash
run: |
uv venv
uv pip install "${{ steps.build.outputs.wheel-file }}"

- name: Test package as CLI tool
if: inputs.test-package == 'true'
shell: bash
run: |
uv venv --python ${{ inputs.python-version }}
uv pip install "${{ steps.build.outputs.wheel-file }}[cli]"
uv run ${{ inputs.cli-test-command }}

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build-test-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ jobs:
test-package: "true"
uv-version: "0.8.22"
cli-test-command: "twyn --version"
python-version: "3.10"
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1

- name: Install the project
run: uv sync --locked --group dev
run: uv sync --locked --group dev --python 3.10

- name: Cache mypy cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.1
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
publish: "false"
uv-version: ${{ env.UV_VERSION }}
cli-test-command: "twyn --version"
python-version: "3.10"

- name: Upload package artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.13"]
python-version: ["3.10", "3.14"]
runs-on: [ubuntu-latest]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG PYTHON_IMAGE=3.13-slim@sha256:1020ca463dc51c26bbad49de85dbb2986d93b71050102f3fa2a7f0fc4c2ea81e
ARG PYTHON_IMAGE=3.14-slim@sha256:4ed33101ee7ec299041cc41dd268dae17031184be94384b1ce7936dc4e5dead3

# --------------- `base` stage ---------------
FROM python:${PYTHON_IMAGE} AS base
Expand Down
9 changes: 5 additions & 4 deletions dependencies/scripts/download_packages.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import json
import logging
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Optional
from typing import Any
from zoneinfo import ZoneInfo

import click
Expand Down Expand Up @@ -33,8 +34,8 @@ class ServerError(Exception):
@dataclass(frozen=True)
class Ecosystem:
url: str
params: Optional[dict[str, Any]]
pages: Optional[int]
params: dict[str, Any] | None
pages: int | None
parser: Callable[[dict[str, Any]], list[str]]


Expand Down Expand Up @@ -88,7 +89,7 @@ def download(


def get_packages(
base_url: str, parser: Callable[[dict[str, Any]], list[str]], params: Optional[dict[str, Any]] = None
base_url: str, parser: Callable[[dict[str, Any]], list[str]], params: dict[str, Any] | None = None
) -> list[str]:
for attempt in stamina.retry_context(
on=(httpx.TransportError, httpx.TimeoutException, ServerError),
Expand Down
8 changes: 4 additions & 4 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# VARIABLE DEFINITIONS
venv := ".venv"
python_version :="3.13"
python_version :="3.10"
run := "uv run"

venv-exists := path_exists(venv)
venv_exists := path_exists(venv)

target_dirs := "src tests dependencies"

Expand All @@ -14,9 +14,9 @@ help:

# Create a new venv with all the dependencies groups
venv:
@if ! {{ venv-exists }}; \
@if ! {{ venv_exists }}; \
then \
uv sync --frozen --all-extras --all-groups; \
uv sync --frozen --all-extras --all-groups --python {{ python_version }}; \
fi

# Cleans all artifacts generated while running this project, including the virtualenv.
Expand Down
8 changes: 3 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ maintainers = [
{ name = "Daniel Sanz", email = "imsdn4z@gmail.com" },
{ name = "Sergio Castillo", email = "s.cast.lara@gmail.com" },
]
requires-python = "<4,>=3.9"
requires-python = "<4,>=3.10"
dependencies = [
"requests<3.0.0,>=2.32.4",
"rapidfuzz<4.0.0,>=2.13.7",
"pyparsing<4.0.0,>=3.2.3",
"tomlkit<0.14.0,>=0.11.6",
"tomli<3.0.0,>=2.2.1; python_version < \"3.13\"",
"pydantic>=2.11.7,<3.0.0",
"pyyaml>=6.0.2",
]
Expand All @@ -30,7 +29,6 @@ cli = [
"rich<15.0.0,>=14.0.0",
]


[tool.hatch.version]
path = "VERSION"
pattern = "v(?P<version>[^\\s]+)"
Expand Down Expand Up @@ -70,7 +68,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.ruff]
target-version = "py39"
target-version = "py310"
line-length = 120

src = ["twyn", "tests"]
Expand Down Expand Up @@ -115,7 +113,7 @@ ignore = [
convention = "pep257"

[tool.mypy]
python_version = "3.9"
python_version = "3.10"
ignore_missing_imports = true
namespace_packages = true
explicit_package_bases = true
Expand Down
4 changes: 1 addition & 3 deletions src/twyn/base/constants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Literal

from typing_extensions import TypeAlias
from typing import TYPE_CHECKING, Literal, TypeAlias

from twyn import dependency_parser
from twyn.trusted_packages import selectors
Expand Down
4 changes: 2 additions & 2 deletions src/twyn/base/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import IO, Any, Optional
from typing import IO, Any

logger = logging.getLogger("twyn")

Expand Down Expand Up @@ -28,7 +28,7 @@ class CliError(click.ClickException):
def __init__(self, message: str = "") -> None:
super().__init__(message)

def show(self, file: Optional[IO[Any]] = None) -> None:
def show(self, file: IO[Any] | None = None) -> None:
"""Display the error message."""
logger.debug(self.format_message(), exc_info=True)
logger.error(self.format_message(), exc_info=False)
Expand Down
9 changes: 4 additions & 5 deletions src/twyn/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
import sys
from typing import Optional

from twyn.__version__ import __version__
from twyn.base.constants import (
Expand Down Expand Up @@ -129,13 +128,13 @@ def run( # noqa: C901
selector_method: str,
v: bool,
vv: bool,
no_cache: Optional[bool],
no_cache: bool | None,
no_track: bool,
json: bool,
package_ecosystem: Optional[str],
package_ecosystem: str | None,
recursive: bool,
pypi_source: Optional[str],
npm_source: Optional[str],
pypi_source: str | None,
npm_source: str | None,
) -> int:
if vv:
logger.setLevel(logging.DEBUG)
Expand Down
44 changes: 22 additions & 22 deletions src/twyn/config/config_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import asdict, dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Optional, Union
from typing import Any

from tomlkit import TOMLDocument, dumps, load, table

Expand Down Expand Up @@ -38,55 +38,55 @@ class TwynConfiguration:
"""Method for selecting similar packages."""
allowlist: set[str]
"""Set of package names to allow without checking."""
pypi_source: Optional[str]
pypi_source: str | None
"""Alternative PyPI source URL."""
npm_source: Optional[str]
npm_source: str | None
"""Alternative npm source URL."""
use_cache: bool
"""Whether to use cached trusted packages."""
package_ecosystem: Optional[PackageEcosystems]
package_ecosystem: PackageEcosystems | None
"""Target package ecosystem for analysis."""
recursive: Optional[bool]
recursive: bool | None
"""Whether to recursively search for dependency files."""


@dataclass
class ReadTwynConfiguration:
"""Configuration for twyn as set by the user. It may have None values."""

dependency_files: Optional[set[str]] = field(default_factory=set)
dependency_files: set[str] | None = field(default_factory=set)
"""Optional set of dependency file paths to analyze."""
selector_method: Optional[str] = None
selector_method: str | None = None
"""Optional method for selecting similar packages."""
allowlist: set[str] = field(default_factory=set)
"""Set of package names to allow without checking."""
pypi_source: Optional[str] = None
pypi_source: str | None = None
"""Optional alternative PyPI source URL."""
npm_source: Optional[str] = None
npm_source: str | None = None
"""Optional alternative npm source URL."""
use_cache: Optional[bool] = None
use_cache: bool | None = None
"""Optional setting for using cached trusted packages."""
package_ecosystem: Optional[PackageEcosystems] = None
package_ecosystem: PackageEcosystems | None = None
"""Optional target package ecosystem for analysis."""
recursive: Optional[bool] = None
recursive: bool | None = None
"""Optional setting for recursive dependency file search."""


class ConfigHandler:
"""Manage reading and writing configurations for Twyn."""

def __init__(self, file_handler: Optional[FileHandler] = None) -> None:
def __init__(self, file_handler: FileHandler | None = None) -> None:
self.file_handler = file_handler

def resolve_config( # noqa: C901, PLR0912
self,
selector_method: Optional[str] = None,
dependency_files: Optional[set[str]] = None,
use_cache: Optional[bool] = None,
package_ecosystem: Optional[PackageEcosystems] = None,
recursive: Optional[bool] = None,
pypi_source: Optional[str] = None,
npm_source: Optional[str] = None,
selector_method: str | None = None,
dependency_files: set[str] | None = None,
use_cache: bool | None = None,
package_ecosystem: PackageEcosystems | None = None,
recursive: bool | None = None,
pypi_source: str | None = None,
npm_source: str | None = None,
) -> TwynConfiguration:
"""Resolve the configuration for Twyn.

Expand Down Expand Up @@ -244,10 +244,10 @@ def get_default_config_file_path() -> str:
return DEFAULT_PROJECT_TOML_FILE


def _serialize_config(x: Any) -> Union[Any, str, list[Any]]:
def _serialize_config(x: Any) -> Any | str | list[Any]:
"""Serialize configuration values for TOML format."""

def _value_to_for_config(v: Any) -> Union[str, list[Any], Any]:
def _value_to_for_config(v: Any) -> str | list[Any] | Any:
if isinstance(v, Enum):
return v.name
elif isinstance(v, set):
Expand Down
3 changes: 1 addition & 2 deletions src/twyn/dependency_managers/managers/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

from twyn.trusted_packages.references.base import AbstractPackageReference

Expand Down Expand Up @@ -30,7 +29,7 @@ def matches_ecosystem_name(cls, name: str) -> bool:
return cls.name == Path(name).name.lower()

@classmethod
def get_alternative_source(cls, sources: dict[str, str]) -> Optional[str]:
def get_alternative_source(cls, sources: dict[str, str]) -> str | None:
"""Get alternative source URL for this ecosystem from sources dict."""
match = [x for x in sources if x == cls.name]

Expand Down
3 changes: 1 addition & 2 deletions src/twyn/dependency_parser/dependency_selector.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
from pathlib import Path
from typing import Optional

from twyn.base.constants import DEPENDENCY_FILE_MAPPING
from twyn.dependency_parser.exceptions import (
Expand All @@ -14,7 +13,7 @@
class DependencySelector:
"""Select and provide parsers for dependency files."""

def __init__(self, dependency_files: Optional[set[str]] = None, root_path: str = ".") -> None:
def __init__(self, dependency_files: set[str] | None = None, root_path: str = ".") -> None:
self.dependency_files = dependency_files or set()
self.root_path = root_path

Expand Down
12 changes: 4 additions & 8 deletions src/twyn/dependency_parser/parsers/lock_parser.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import sys
import tomlkit

from twyn.dependency_parser.parsers.abstract_parser import AbstractParser
from twyn.dependency_parser.parsers.constants import POETRY_LOCK, UV_LOCK

if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib


class TomlLockParser(AbstractParser):
"""Parser for TOML-based lock files."""

def parse(self) -> set[str]:
"""Parse dependencies names and map them to a set."""
data = tomllib.loads(self.file_handler.read())
return {dependency["name"] for dependency in data["package"]}
data = tomlkit.parse(self.file_handler.read())
packages = data.get("package", [])
return {pkg["name"] for pkg in packages if isinstance(pkg, dict) and "name" in pkg}


class PoetryLockParser(TomlLockParser):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import re
from pathlib import Path
from typing import Union

from typing_extensions import override

Expand Down Expand Up @@ -37,7 +36,7 @@ def parse(self) -> set[str]:
"""
return self._parse_internal(self.file_path, seen_files=set())

def _parse_internal(self, source: Union[str, Path], seen_files: set[Path]) -> set[str]:
def _parse_internal(self, source: str | Path, seen_files: set[Path]) -> set[str]:
"""Parse requirements file and handle includes recursively."""
packages: set[str] = set()
base_dir = Path(source).parent if isinstance(source, Path) else Path(".")
Expand Down
Loading
Loading