Skip to content

Commit 3e1c23e

Browse files
Add nox task to verify dependency declarations (#236)
1 parent 9876aa8 commit 3e1c23e

File tree

5 files changed

+229
-1
lines changed

5 files changed

+229
-1
lines changed

doc/changes/unreleased.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# Unreleased
2+
3+
## ✨ Added
4+
5+
* #233: Added nox task to verify dependency declarations

doc/user_guide/getting_started.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ You are ready to use the toolbox. With *nox -l* you can list all available tasks
193193
- lint:code -> Runs the static code analyzer on the project
194194
- lint:typing -> Runs the type checker on the project
195195
- lint:security -> Runs the security linter on the project
196+
- lint:dependencies -> Checks if only valid sources of dependencies are used
196197
- docs:multiversion -> Builds the multiversion project documentation
197198
- docs:build -> Builds the project documentation
198199
- docs:open -> Opens the built project documentation

exasol/toolbox/nox/_lint.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
from __future__ import annotations
22

3-
from typing import Iterable
3+
from typing import (
4+
Iterable,
5+
List,
6+
Dict
7+
)
48

59
import nox
610
from nox import Session
711

812
from exasol.toolbox.nox._shared import python_files
913
from noxconfig import PROJECT_CONFIG
1014

15+
from pathlib import Path
16+
import rich.console
17+
import tomlkit
18+
import sys
19+
1120

1221
def _pylint(session: Session, files: Iterable[str]) -> None:
1322
session.run(
@@ -65,6 +74,61 @@ def _security_lint(session: Session, files: Iterable[str]) -> None:
6574
)
6675

6776

77+
class Dependencies:
78+
def __init__(self, illegal: Dict[str, List[str]] | None):
79+
self._illegal = illegal or {}
80+
81+
@staticmethod
82+
def parse(pyproject_toml: str) -> "Dependencies":
83+
def _source_filter(version) -> bool:
84+
ILLEGAL_SPECIFIERS = ['url', 'git', 'path']
85+
return any(
86+
specifier in version
87+
for specifier in ILLEGAL_SPECIFIERS
88+
)
89+
90+
def find_illegal(part) -> List[str]:
91+
return [
92+
f"{name} = {version}"
93+
for name, version in part.items()
94+
if _source_filter(version)
95+
]
96+
97+
illegal: Dict[str, List[str]] = {}
98+
toml = tomlkit.loads(pyproject_toml)
99+
poetry = toml.get("tool", {}).get("poetry", {})
100+
101+
part = poetry.get("dependencies", {})
102+
if illegal_group := find_illegal(part):
103+
illegal["tool.poetry.dependencies"] = illegal_group
104+
105+
part = poetry.get("dev", {}).get("dependencies", {})
106+
if illegal_group := find_illegal(part):
107+
illegal["tool.poetry.dev.dependencies"] = illegal_group
108+
109+
part = poetry.get("group", {})
110+
for group, content in part.items():
111+
illegal_group = find_illegal(content.get("dependencies", {}))
112+
if illegal_group:
113+
illegal[f"tool.poetry.group.{group}.dependencies"] = illegal_group
114+
return Dependencies(illegal)
115+
116+
@property
117+
def illegal(self) -> Dict[str, List[str]]:
118+
return self._illegal
119+
120+
121+
def report_illegal(illegal: Dict[str, List[str]], console: rich.console.Console):
122+
count = sum(len(deps) for deps in illegal.values())
123+
suffix = "y" if count == 1 else "ies"
124+
console.print(f"{count} illegal dependenc{suffix}\n", style="red")
125+
for section, dependencies in illegal.items():
126+
console.print(f"\\[{section}]", style="red")
127+
for dependency in dependencies:
128+
console.print(dependency, style="red")
129+
console.print("")
130+
131+
68132
@nox.session(name="lint:code", python=False)
69133
def lint(session: Session) -> None:
70134
"Runs the static code analyzer on the project"
@@ -84,3 +148,14 @@ def security_lint(session: Session) -> None:
84148
"""Runs the security linter on the project"""
85149
py_files = [f"{file}" for file in python_files(PROJECT_CONFIG.root)]
86150
_security_lint(session, list(filter(lambda file: "test" not in file, py_files)))
151+
152+
153+
@nox.session(name="lint:dependencies", python=False)
154+
def dependency_check(session: Session) -> None:
155+
"""Checks if only valid sources of dependencies are used"""
156+
content = Path(PROJECT_CONFIG.root, "pyproject.toml").read_text()
157+
dependencies = Dependencies.parse(content)
158+
console = rich.console.Console()
159+
if illegal := dependencies.illegal:
160+
report_illegal(illegal, console)
161+
sys.exit(1)

exasol/toolbox/nox/tasks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,6 @@ def check(session: Session) -> None:
6868
python_files,
6969
)
7070

71+
7172
# isort: on
7273
# fmt: on
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import pytest
2+
import rich.console
3+
4+
from exasol.toolbox.nox._lint import Dependencies, report_illegal
5+
6+
7+
@pytest.mark.parametrize(
8+
"toml,expected",
9+
[
10+
(
11+
"""
12+
""",
13+
{}
14+
),
15+
(
16+
"""
17+
[tool.poetry.dependencies]
18+
python = "^3.8"
19+
example-url1 = {url = "https://example.com/my-package-0.1.0.tar.gz"}
20+
21+
[tool.poetry.dev.dependencies]
22+
nox = ">=2022.8.7"
23+
example-url2 = {url = "https://example.com/my-package-0.2.0.tar.gz"}
24+
25+
[tool.poetry.group.test.dependencies]
26+
sphinx = ">=5.3,<8"
27+
example-git = {git = "git@github.com:requests/requests.git"}
28+
29+
[tool.poetry.group.dev.dependencies]
30+
pytest = ">=7.2.2,<9"
31+
example-path1 = {path = "../my-package/dist/my-package-0.1.0.tar.gz"}
32+
""",
33+
{
34+
"tool.poetry.dependencies": ["example-url1 = {'url': 'https://example.com/my-package-0.1.0.tar.gz'}"],
35+
"tool.poetry.dev.dependencies": ["example-url2 = {'url': 'https://example.com/my-package-0.2.0.tar.gz'}"],
36+
"tool.poetry.group.test.dependencies": ["example-git = {'git': 'git@github.com:requests/requests.git'}"],
37+
"tool.poetry.group.dev.dependencies": ["example-path1 = {'path': '../my-package/dist/my-package-0.1.0.tar.gz'}"],
38+
}
39+
),
40+
(
41+
"""
42+
[tool.poetry.dev.dependencies]
43+
nox = ">=2022.8.7"
44+
example-url2 = {url = "https://example.com/my-package-0.2.0.tar.gz"}
45+
46+
[tool.poetry.group.test.dependencies]
47+
sphinx = ">=5.3,<8"
48+
example-git = {git = "git@github.com:requests/requests.git"}
49+
50+
[tool.poetry.group.dev.dependencies]
51+
pytest = ">=7.2.2,<9"
52+
example-path1 = {path = "../my-package/dist/my-package-0.1.0.tar.gz"}
53+
""",
54+
{
55+
"tool.poetry.dev.dependencies": ["example-url2 = {'url': 'https://example.com/my-package-0.2.0.tar.gz'}"],
56+
"tool.poetry.group.test.dependencies": ["example-git = {'git': 'git@github.com:requests/requests.git'}"],
57+
"tool.poetry.group.dev.dependencies": ["example-path1 = {'path': '../my-package/dist/my-package-0.1.0.tar.gz'}"],
58+
}
59+
),
60+
(
61+
"""
62+
[tool.poetry.dependencies]
63+
python = "^3.8"
64+
example-url1 = {url = "https://example.com/my-package-0.1.0.tar.gz"}
65+
66+
[tool.poetry.group.test.dependencies]
67+
sphinx = ">=5.3,<8"
68+
example-git = {git = "git@github.com:requests/requests.git"}
69+
70+
[tool.poetry.group.dev.dependencies]
71+
pytest = ">=7.2.2,<9"
72+
example-path1 = {path = "../my-package/dist/my-package-0.1.0.tar.gz"}
73+
""",
74+
{
75+
"tool.poetry.dependencies": ["example-url1 = {'url': 'https://example.com/my-package-0.1.0.tar.gz'}"],
76+
"tool.poetry.group.test.dependencies": ["example-git = {'git': 'git@github.com:requests/requests.git'}"],
77+
"tool.poetry.group.dev.dependencies": ["example-path1 = {'path': '../my-package/dist/my-package-0.1.0.tar.gz'}"],
78+
}
79+
),
80+
(
81+
"""
82+
[tool.poetry.dependencies]
83+
python = "^3.8"
84+
example-url1 = {url = "https://example.com/my-package-0.1.0.tar.gz"}
85+
86+
[tool.poetry.dev.dependencies]
87+
nox = ">=2022.8.7"
88+
example-url2 = {url = "https://example.com/my-package-0.2.0.tar.gz"}
89+
""",
90+
{
91+
"tool.poetry.dependencies": ["example-url1 = {'url': 'https://example.com/my-package-0.1.0.tar.gz'}"],
92+
"tool.poetry.dev.dependencies": ["example-url2 = {'url': 'https://example.com/my-package-0.2.0.tar.gz'}"],
93+
}
94+
)
95+
]
96+
)
97+
def test_dependency_check_parse(toml, expected):
98+
dependencies = dependencies = Dependencies.parse(toml)
99+
assert dependencies.illegal == expected
100+
101+
102+
@pytest.mark.parametrize(
103+
"toml,expected",
104+
[
105+
(
106+
"""
107+
[tool.poetry.dependencies]
108+
python = "^3.8"
109+
example-url1 = {url = "https://example.com/my-package-0.1.0.tar.gz"}
110+
111+
[tool.poetry.dev.dependencies]
112+
nox = ">=2022.8.7"
113+
example-url2 = {url = "https://example.com/my-package-0.2.0.tar.gz"}
114+
115+
[tool.poetry.group.test.dependencies]
116+
sphinx = ">=5.3,<8"
117+
example-git = {git = "git@github.com:requests/requests.git"}
118+
119+
[tool.poetry.group.dev.dependencies]
120+
pytest = ">=7.2.2,<9"
121+
example-path1 = {path = "../my-package/dist/my-package-0.1.0.tar.gz"}
122+
example-path2 = {path = "../my-package/dist/my-package-0.2.0.tar.gz"}
123+
""",
124+
"""5 illegal dependencies
125+
126+
[tool.poetry.dependencies]
127+
example-url1 = {'url': 'https://example.com/my-package-0.1.0.tar.gz'}
128+
129+
[tool.poetry.dev.dependencies]
130+
example-url2 = {'url': 'https://example.com/my-package-0.2.0.tar.gz'}
131+
132+
[tool.poetry.group.test.dependencies]
133+
example-git = {'git': 'git@github.com:requests/requests.git'}
134+
135+
[tool.poetry.group.dev.dependencies]
136+
example-path1 = {'path': '../my-package/dist/my-package-0.1.0.tar.gz'}
137+
example-path2 = {'path': '../my-package/dist/my-package-0.2.0.tar.gz'}
138+
139+
"""
140+
),
141+
]
142+
)
143+
def test_dependencies_check_report(toml, expected, capsys):
144+
console = rich.console.Console()
145+
dependencies = Dependencies.parse(toml)
146+
report_illegal(dependencies.illegal, console)
147+
assert capsys.readouterr().out == expected

0 commit comments

Comments
 (0)