Skip to content

Commit

Permalink
Add nox task to verify dependency declarations (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jannis-Mittenzwei authored Nov 15, 2024
1 parent 9876aa8 commit 3e1c23e
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 1 deletion.
4 changes: 4 additions & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Unreleased

## ✨ Added

* #233: Added nox task to verify dependency declarations
1 change: 1 addition & 0 deletions doc/user_guide/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ You are ready to use the toolbox. With *nox -l* you can list all available tasks
- lint:code -> Runs the static code analyzer on the project
- lint:typing -> Runs the type checker on the project
- lint:security -> Runs the security linter on the project
- lint:dependencies -> Checks if only valid sources of dependencies are used
- docs:multiversion -> Builds the multiversion project documentation
- docs:build -> Builds the project documentation
- docs:open -> Opens the built project documentation
Expand Down
77 changes: 76 additions & 1 deletion exasol/toolbox/nox/_lint.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
from __future__ import annotations

from typing import Iterable
from typing import (
Iterable,
List,
Dict
)

import nox
from nox import Session

from exasol.toolbox.nox._shared import python_files
from noxconfig import PROJECT_CONFIG

from pathlib import Path
import rich.console
import tomlkit
import sys


def _pylint(session: Session, files: Iterable[str]) -> None:
session.run(
Expand Down Expand Up @@ -65,6 +74,61 @@ def _security_lint(session: Session, files: Iterable[str]) -> None:
)


class Dependencies:
def __init__(self, illegal: Dict[str, List[str]] | None):
self._illegal = illegal or {}

@staticmethod
def parse(pyproject_toml: str) -> "Dependencies":
def _source_filter(version) -> bool:
ILLEGAL_SPECIFIERS = ['url', 'git', 'path']
return any(
specifier in version
for specifier in ILLEGAL_SPECIFIERS
)

def find_illegal(part) -> List[str]:
return [
f"{name} = {version}"
for name, version in part.items()
if _source_filter(version)
]

illegal: Dict[str, List[str]] = {}
toml = tomlkit.loads(pyproject_toml)
poetry = toml.get("tool", {}).get("poetry", {})

part = poetry.get("dependencies", {})
if illegal_group := find_illegal(part):
illegal["tool.poetry.dependencies"] = illegal_group

part = poetry.get("dev", {}).get("dependencies", {})
if illegal_group := find_illegal(part):
illegal["tool.poetry.dev.dependencies"] = illegal_group

part = poetry.get("group", {})
for group, content in part.items():
illegal_group = find_illegal(content.get("dependencies", {}))
if illegal_group:
illegal[f"tool.poetry.group.{group}.dependencies"] = illegal_group
return Dependencies(illegal)

@property
def illegal(self) -> Dict[str, List[str]]:
return self._illegal


def report_illegal(illegal: Dict[str, List[str]], console: rich.console.Console):
count = sum(len(deps) for deps in illegal.values())
suffix = "y" if count == 1 else "ies"
console.print(f"{count} illegal dependenc{suffix}\n", style="red")
for section, dependencies in illegal.items():
console.print(f"\\[{section}]", style="red")
for dependency in dependencies:
console.print(dependency, style="red")
console.print("")


@nox.session(name="lint:code", python=False)
def lint(session: Session) -> None:
"Runs the static code analyzer on the project"
Expand All @@ -84,3 +148,14 @@ def security_lint(session: Session) -> None:
"""Runs the security linter on the project"""
py_files = [f"{file}" for file in python_files(PROJECT_CONFIG.root)]
_security_lint(session, list(filter(lambda file: "test" not in file, py_files)))


@nox.session(name="lint:dependencies", python=False)
def dependency_check(session: Session) -> None:
"""Checks if only valid sources of dependencies are used"""
content = Path(PROJECT_CONFIG.root, "pyproject.toml").read_text()
dependencies = Dependencies.parse(content)
console = rich.console.Console()
if illegal := dependencies.illegal:
report_illegal(illegal, console)
sys.exit(1)
1 change: 1 addition & 0 deletions exasol/toolbox/nox/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@ def check(session: Session) -> None:
python_files,
)


# isort: on
# fmt: on
147 changes: 147 additions & 0 deletions test/unit/dependencies_check_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import pytest
import rich.console

from exasol.toolbox.nox._lint import Dependencies, report_illegal


@pytest.mark.parametrize(
"toml,expected",
[
(
"""
""",
{}
),
(
"""
[tool.poetry.dependencies]
python = "^3.8"
example-url1 = {url = "https://example.com/my-package-0.1.0.tar.gz"}
[tool.poetry.dev.dependencies]
nox = ">=2022.8.7"
example-url2 = {url = "https://example.com/my-package-0.2.0.tar.gz"}
[tool.poetry.group.test.dependencies]
sphinx = ">=5.3,<8"
example-git = {git = "[email protected]:requests/requests.git"}
[tool.poetry.group.dev.dependencies]
pytest = ">=7.2.2,<9"
example-path1 = {path = "../my-package/dist/my-package-0.1.0.tar.gz"}
""",
{
"tool.poetry.dependencies": ["example-url1 = {'url': 'https://example.com/my-package-0.1.0.tar.gz'}"],
"tool.poetry.dev.dependencies": ["example-url2 = {'url': 'https://example.com/my-package-0.2.0.tar.gz'}"],
"tool.poetry.group.test.dependencies": ["example-git = {'git': '[email protected]:requests/requests.git'}"],
"tool.poetry.group.dev.dependencies": ["example-path1 = {'path': '../my-package/dist/my-package-0.1.0.tar.gz'}"],
}
),
(
"""
[tool.poetry.dev.dependencies]
nox = ">=2022.8.7"
example-url2 = {url = "https://example.com/my-package-0.2.0.tar.gz"}
[tool.poetry.group.test.dependencies]
sphinx = ">=5.3,<8"
example-git = {git = "[email protected]:requests/requests.git"}
[tool.poetry.group.dev.dependencies]
pytest = ">=7.2.2,<9"
example-path1 = {path = "../my-package/dist/my-package-0.1.0.tar.gz"}
""",
{
"tool.poetry.dev.dependencies": ["example-url2 = {'url': 'https://example.com/my-package-0.2.0.tar.gz'}"],
"tool.poetry.group.test.dependencies": ["example-git = {'git': '[email protected]:requests/requests.git'}"],
"tool.poetry.group.dev.dependencies": ["example-path1 = {'path': '../my-package/dist/my-package-0.1.0.tar.gz'}"],
}
),
(
"""
[tool.poetry.dependencies]
python = "^3.8"
example-url1 = {url = "https://example.com/my-package-0.1.0.tar.gz"}
[tool.poetry.group.test.dependencies]
sphinx = ">=5.3,<8"
example-git = {git = "[email protected]:requests/requests.git"}
[tool.poetry.group.dev.dependencies]
pytest = ">=7.2.2,<9"
example-path1 = {path = "../my-package/dist/my-package-0.1.0.tar.gz"}
""",
{
"tool.poetry.dependencies": ["example-url1 = {'url': 'https://example.com/my-package-0.1.0.tar.gz'}"],
"tool.poetry.group.test.dependencies": ["example-git = {'git': '[email protected]:requests/requests.git'}"],
"tool.poetry.group.dev.dependencies": ["example-path1 = {'path': '../my-package/dist/my-package-0.1.0.tar.gz'}"],
}
),
(
"""
[tool.poetry.dependencies]
python = "^3.8"
example-url1 = {url = "https://example.com/my-package-0.1.0.tar.gz"}
[tool.poetry.dev.dependencies]
nox = ">=2022.8.7"
example-url2 = {url = "https://example.com/my-package-0.2.0.tar.gz"}
""",
{
"tool.poetry.dependencies": ["example-url1 = {'url': 'https://example.com/my-package-0.1.0.tar.gz'}"],
"tool.poetry.dev.dependencies": ["example-url2 = {'url': 'https://example.com/my-package-0.2.0.tar.gz'}"],
}
)
]
)
def test_dependency_check_parse(toml, expected):
dependencies = dependencies = Dependencies.parse(toml)
assert dependencies.illegal == expected


@pytest.mark.parametrize(
"toml,expected",
[
(
"""
[tool.poetry.dependencies]
python = "^3.8"
example-url1 = {url = "https://example.com/my-package-0.1.0.tar.gz"}
[tool.poetry.dev.dependencies]
nox = ">=2022.8.7"
example-url2 = {url = "https://example.com/my-package-0.2.0.tar.gz"}
[tool.poetry.group.test.dependencies]
sphinx = ">=5.3,<8"
example-git = {git = "[email protected]:requests/requests.git"}
[tool.poetry.group.dev.dependencies]
pytest = ">=7.2.2,<9"
example-path1 = {path = "../my-package/dist/my-package-0.1.0.tar.gz"}
example-path2 = {path = "../my-package/dist/my-package-0.2.0.tar.gz"}
""",
"""5 illegal dependencies
[tool.poetry.dependencies]
example-url1 = {'url': 'https://example.com/my-package-0.1.0.tar.gz'}
[tool.poetry.dev.dependencies]
example-url2 = {'url': 'https://example.com/my-package-0.2.0.tar.gz'}
[tool.poetry.group.test.dependencies]
example-git = {'git': '[email protected]:requests/requests.git'}
[tool.poetry.group.dev.dependencies]
example-path1 = {'path': '../my-package/dist/my-package-0.1.0.tar.gz'}
example-path2 = {'path': '../my-package/dist/my-package-0.2.0.tar.gz'}
"""
),
]
)
def test_dependencies_check_report(toml, expected, capsys):
console = rich.console.Console()
dependencies = Dependencies.parse(toml)
report_illegal(dependencies.illegal, console)
assert capsys.readouterr().out == expected

0 comments on commit 3e1c23e

Please sign in to comment.