diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 79e701b..e105f6a 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -1 +1,5 @@ # Unreleased + +## ✨ Added + +#233: Added nox task to verify dependency-declarations diff --git a/exasol/toolbox/nox/_dependencies_check.py b/exasol/toolbox/nox/_dependencies_check.py new file mode 100644 index 0000000..d508576 --- /dev/null +++ b/exasol/toolbox/nox/_dependencies_check.py @@ -0,0 +1,81 @@ +import tomlkit +import sys +from pathlib import Path +import nox +from nox import Session +from noxconfig import PROJECT_CONFIG +from typing import ( + List, + Dict +) +import rich.console + + +@nox.session(name="dependencies-check", python=False) +def dependency_check(session: Session) -> None: + content = Path(PROJECT_CONFIG.root, "pyproject.toml").read_text() + dependencies = Dependencies(content).parse() + console = rich.console.Console() + if illegal := dependencies.illegal: + report_illegal(illegal, console) + sys.exit(1) + + +class Dependencies: + ILLEGAL_DEPENDENCIES = ['url', 'git', 'path'] + + def __init__(self, pyproject_toml: str): + self.illegal_dict: Dict[str, List[str]] = {} + self.content = pyproject_toml + + def parse(self) -> "Dependencies": + def source_filter(version) -> bool: + for f in self.ILLEGAL_DEPENDENCIES: + if f in version: + return True + return False + + def extract_dependencies(section) -> List[str]: + dependencies = [] + for name, version in section.items(): + if source_filter(version): + dependencies.append(f"{name} = {version}") + return dependencies + + illegal: Dict[str, List[str]] = {} + toml = tomlkit.loads(self.content) + poetry = toml.get("tool", {}).get("poetry", {}) + + part = poetry.get("dependencies", {}) + dependencies_list = extract_dependencies(part) + if dependencies_list: + illegal["tool.poetry.dependencies"] = dependencies_list + + part = poetry.get("dev", {}).get("dependencies", {}) + dependencies_list = extract_dependencies(part) + if dependencies_list: + illegal["tool.poetry.dev.dependencies"] = dependencies_list + + part = poetry.get("group", {}) + for group, content in part.items(): + dependencies_list = extract_dependencies(content.get("dependencies", {})) + if dependencies_list: + illegal[f"tool.poetry.group.{group}.dependencies"] = dependencies_list + + self.illegal_dict = illegal + return self + + @property + def illegal(self) -> Dict[str, List[str]]: + return self.illegal_dict + + +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("") diff --git a/exasol/toolbox/nox/tasks.py b/exasol/toolbox/nox/tasks.py index d9cb27d..9161a2c 100644 --- a/exasol/toolbox/nox/tasks.py +++ b/exasol/toolbox/nox/tasks.py @@ -51,6 +51,9 @@ integration_tests, unit_tests, ) +from exasol.toolbox.nox._dependencies_check import ( + dependency_check +) from noxconfig import PROJECT_CONFIG diff --git a/test/unit/dependencies_check_test.py b/test/unit/dependencies_check_test.py new file mode 100644 index 0000000..db76c19 --- /dev/null +++ b/test/unit/dependencies_check_test.py @@ -0,0 +1,147 @@ +import pytest +import rich.console + +from exasol.toolbox.nox._dependencies_check 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 = "git@github.com: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': 'git@github.com: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 = "git@github.com: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': 'git@github.com: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 = "git@github.com: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': 'git@github.com: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(toml).parse() + 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 = "git@github.com: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': 'git@github.com: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(toml).parse() + report_illegal(dependencies.illegal, console) + assert capsys.readouterr().out == expected