Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d73ece5
Remove pylint addition which is included already in lint:code
ArBridgeman Jul 22, 2025
40f0d6a
Remove unused secrets.GITHUB_TOKEN from checks.yml
ArBridgeman Jul 22, 2025
02e5037
Add tracking types for DependencyChange with tests
ArBridgeman Jul 22, 2025
55ac847
Switch dependencies from list to dict of packages for O(1) retrieval
ArBridgeman Jul 24, 2025
2a50de1
Update licenses to work with dictionary and have shared special strin…
ArBridgeman Jul 24, 2025
f870236
Add dependency updates to changelog
ArBridgeman Jul 24, 2025
4077c88
Ignore typing as if-elif safeguard None from getting to those lines
ArBridgeman Jul 24, 2025
0517262
Add changelog entry
ArBridgeman Jul 24, 2025
3a8243d
Pass on OrderedDict type
ArBridgeman Jul 24, 2025
4cb1bf6
Pass on OrderedDict type
ArBridgeman Jul 24, 2025
c84bde9
Restore order to licenses as dict keys to set loses order
ArBridgeman Jul 24, 2025
983afca
Fix indent and make date variable; could alternately add freeze gun o…
ArBridgeman Jul 25, 2025
91dd088
Add types for track_changes.py
ArBridgeman Jul 25, 2025
7a9e640
Rename old to previous for dependencies
ArBridgeman Jul 25, 2025
4801384
Use group to be consistent with what we use in dependency changes and…
ArBridgeman Jul 25, 2025
86b11af
Remove comment as not needed or useful
ArBridgeman Jul 25, 2025
d867d3c
Switch source_path to be correctly named root_path
ArBridgeman Jul 25, 2025
9023f1d
Add output types to methods
ArBridgeman Jul 25, 2025
5265499
Simplify output for _format_table_row
ArBridgeman Jul 25, 2025
5d75414
Simplify output for _format_group_table_header
ArBridgeman Jul 25, 2025
b7c311c
Use normalized name from object instead of import
ArBridgeman Jul 25, 2025
5efc417
Switch from usage of string with += to using append in a list and joi…
ArBridgeman Jul 25, 2025
edf985a
Switch from old_version to previous_version
ArBridgeman Jul 25, 2025
d26f7ae
Switch from old_package to previous_package
ArBridgeman Jul 25, 2025
d5069c4
Switch from str to NormalizedPackageStr as that's what the dict keys are
ArBridgeman Jul 25, 2025
22bc3da
Switch Union[*, None] with Optional[*]
ArBridgeman Jul 25, 2025
c339a5f
Add comment to make clearer what the operation does
ArBridgeman Jul 25, 2025
96b98f8
Add on description of Changelogs arguments
ArBridgeman Jul 25, 2025
cfae785
Improve terminology & adapt _describe_dependency_changes to be simpler
ArBridgeman Jul 25, 2025
31b48bc
Add deterministic sorting to versioned changes file
ArBridgeman Jul 25, 2025
898e10c
Simplify _create_versioned_changelog and fix moved type
ArBridgeman Jul 25, 2025
b011f5f
Remove unneeded, second cleancode() usage
ArBridgeman Jul 25, 2025
3524c45
Switch text from changes to contents
ArBridgeman Jul 25, 2025
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
2 changes: 0 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,6 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }}
Expand Down
4 changes: 4 additions & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Unreleased

## Feature

* #382: Added onto nox session `release:prepare` to append dependency changes between current and latest tag

## Refactoring

* #498: Centralized changelog code relevant for `release:trigger` & robustly tested
7 changes: 2 additions & 5 deletions exasol/toolbox/nox/_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
import sys
from collections.abc import Iterable
from pathlib import Path
from typing import (
Optional,
Union,
)
from typing import Optional

import nox
from nox import Session
Expand Down Expand Up @@ -188,7 +185,7 @@ def _copy_artifacts(source: Path, dest: Path, files: Iterable[str]):


def _prepare_coverage_xml(
session: Session, source: Path, cwd: Union[Path, None] = None
session: Session, source: Path, cwd: Optional[Path] = None
) -> None:
"""
Prepare the coverage XML for input into Sonar
Expand Down
11 changes: 7 additions & 4 deletions exasol/toolbox/nox/_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from nox import Session

from exasol.toolbox.util.dependencies.licenses import (
licenses,
packages_to_markdown,
PackageLicenseReport,
get_licenses,
)
from exasol.toolbox.util.dependencies.poetry_dependencies import get_dependencies

Expand Down Expand Up @@ -85,8 +85,11 @@ def run(self, session: Session) -> None:
def dependency_licenses(session: Session) -> None:
"""Return the packages with their licenses"""
dependencies = get_dependencies(working_directory=Path())
package_infos = licenses()
print(packages_to_markdown(dependencies=dependencies, packages=package_infos))
licenses = get_licenses()
license_markdown = PackageLicenseReport(
dependencies=dependencies, licenses=licenses
)
print(license_markdown.to_markdown())


@nox.session(name="dependency:audit", python=False)
Expand Down
4 changes: 3 additions & 1 deletion exasol/toolbox/nox/_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ def prepare_release(session: Session) -> None:
_ = _update_project_version(session, new_version)

changelogs = Changelogs(
changes_path=PROJECT_CONFIG.doc / "changes", version=new_version
changes_path=PROJECT_CONFIG.doc / "changes",
root_path=PROJECT_CONFIG.root,
version=new_version,
)
changelogs.update_changelogs_for_release()
changed_files = changelogs.get_changed_files()
Expand Down
2 changes: 0 additions & 2 deletions exasol/toolbox/templates/github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }}
Expand Down
129 changes: 63 additions & 66 deletions exasol/toolbox/util/dependencies/licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

import subprocess
import tempfile
from collections import OrderedDict
from dataclasses import dataclass
from inspect import cleandoc
from json import loads
from typing import Optional

from pydantic import field_validator

from exasol.toolbox.util.dependencies.shared_models import Package
from exasol.toolbox.util.dependencies.shared_models import (
NormalizedPackageStr,
Package,
)

LICENSE_MAPPING_TO_ABBREVIATION = {
"BSD License": "BSD",
Expand Down Expand Up @@ -88,20 +93,23 @@ def select_most_restrictive(licenses: list[str]) -> str:
return LICENSE_MAPPING_TO_ABBREVIATION.get(_license, _license)


def _packages_from_json(json: str) -> list[PackageLicense]:
def _packages_from_json(json: str) -> dict[NormalizedPackageStr, PackageLicense]:
packages = loads(json)
return [
PackageLicense(
name=package["Name"],
package_link=package["URL"],
version=package["Version"],
license=package["License"],
)
return {
package_license.normalized_name: package_license
for package in packages
]
if (
package_license := PackageLicense(
name=package["Name"],
package_link=package["URL"],
version=package["Version"],
license=package["License"],
)
)
}


def licenses() -> list[PackageLicense]:
def get_licenses() -> dict[NormalizedPackageStr, PackageLicense]:
with tempfile.NamedTemporaryFile() as file:
subprocess.run(
[
Expand All @@ -117,62 +125,51 @@ def licenses() -> list[PackageLicense]:
return _packages_from_json(file.read().decode())


def packages_to_markdown(
dependencies: dict[str, list], packages: list[PackageLicense]
) -> str:
def heading():
return "# Dependencies\n"
@dataclass(frozen=True)
class PackageLicenseReport:
dependencies: OrderedDict[str, dict[NormalizedPackageStr, Package]]
licenses: dict[NormalizedPackageStr, PackageLicense]

def dependency(
group: str,
group_packages: list[Package],
packages: list[PackageLicense],
) -> str:
def _header(_group: str):
_group = "".join([word.capitalize() for word in _group.strip().split()])
text = f"## {_group} Dependencies\n"
text += "|Package|Version|License|\n"
text += "|---|---|---|\n"
return text

def _rows(
_group_packages: list[Package], _packages: list[PackageLicense]
) -> str:
text = ""
for package in _group_packages:
consistent = filter(
lambda elem: elem.normalized_name == package.normalized_name,
_packages,
)
for content in consistent:
if content.package_link:
text += f"|[{content.name}]({content.package_link})"
else:
text += f"|{content.name}"
text += f"|{content.version}"
if content.license_link:
text += f"|[{content.license}]({content.license_link})|\n"
else:
text += f"|{content.license}|\n"
text += "\n"
return text

_template = cleandoc(
@staticmethod
def _format_group_table_header(group: str) -> str:
return cleandoc(
f"""## `{group}` Dependencies
|Package|Version|License|
|---|---|---|
"""
{header}{rows}
"""
)
return _template.format(
header=_header(group), rows=_rows(group_packages, packages)
)

template = cleandoc(
"""
{heading}{rows}
"""
)

rows = ""
for group in dependencies:
rows += dependency(group, dependencies[group], packages)
return template.format(heading=heading(), rows=rows)
def _format_group_table(
self, group: str, group_package_names: set[NormalizedPackageStr]
) -> str:
group_header = self._format_group_table_header(group=group)

rows = []
for package_name in sorted(group_package_names):
if license_info := self.licenses.get(package_name):
rows.append(self._format_table_row(license_info=license_info))

return f"""{group_header}\n{''.join(rows)}\n"""

@staticmethod
def _format_table_row(license_info: PackageLicense) -> str:
row_package = f"{license_info.name}"
if license_info.package_link:
row_package = f"[{license_info.name}]({license_info.package_link})"

row_license = f"{license_info.license}"
if license_info.license_link:
row_license = f"[{license_info.license}]({license_info.license_link})"

return f"|{row_package}|{license_info.version}|{row_license}|\n"

def to_markdown(self) -> str:
rows = []
for group in self.dependencies:
group_package_names = set(self.dependencies[group].keys())
rows.append(
self._format_group_table(
group=group, group_package_names=group_package_names
)
)
return cleandoc(f"""# Dependencies\n\n{''.join(rows)}""")
43 changes: 28 additions & 15 deletions exasol/toolbox/util/dependencies/poetry_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import subprocess
import tempfile
from collections import OrderedDict
from pathlib import Path
from typing import Optional

Expand All @@ -12,7 +13,10 @@
)
from tomlkit import TOMLDocument

from exasol.toolbox.util.dependencies.shared_models import Package
from exasol.toolbox.util.dependencies.shared_models import (
NormalizedPackageStr,
Package,
)
from exasol.toolbox.util.git import Git


Expand Down Expand Up @@ -92,16 +96,20 @@ def _extract_from_line(line: str) -> Optional[Package]:
return None
return Package(name=split_line[0], version=split_line[1])

def _extract_from_poetry_show(self, output_text: str) -> list[Package]:
return [
package
def _extract_from_poetry_show(
self, output_text: str
) -> dict[NormalizedPackageStr, Package]:
return {
package.normalized_name: package
for line in output_text.splitlines()
if (package := self._extract_from_line(line))
]
}

@property
def direct_dependencies(self) -> dict[str, list[Package]]:
dependencies = {}
def direct_dependencies(
self,
) -> OrderedDict[str, dict[NormalizedPackageStr, Package]]:
dependencies = OrderedDict()
for group in self.groups:
command = (
"poetry",
Expand All @@ -122,7 +130,7 @@ def direct_dependencies(self) -> dict[str, list[Package]]:
return dependencies

@property
def all_dependencies(self) -> dict[str, list[Package]]:
def all_dependencies(self) -> OrderedDict[str, dict[NormalizedPackageStr, Package]]:
command = ("poetry", "show", "--no-truncate")
output = subprocess.run(
command,
Expand All @@ -133,28 +141,33 @@ def all_dependencies(self) -> dict[str, list[Package]]:
)

direct_dependencies = self.direct_dependencies.copy()
transitive_dependencies = []

transitive_dependencies = {}
names_direct_dependencies = {
dep.name
for group_list in direct_dependencies.values()
for dep in group_list
package_name
for group_list in direct_dependencies
for package_name in group_list
}
for line in output.stdout.splitlines():
dep = self._extract_from_line(line=line)
if dep and dep.name not in names_direct_dependencies:
transitive_dependencies.append(dep)
transitive_dependencies[dep.normalized_name] = dep

return direct_dependencies | {TRANSITIVE_GROUP.name: transitive_dependencies}


def get_dependencies(working_directory: Path) -> dict[str, list[Package]]:
def get_dependencies(
working_directory: Path,
) -> OrderedDict[str, dict[NormalizedPackageStr, Package]]:
poetry_dep = PoetryToml.load_from_toml(working_directory=working_directory)
return PoetryDependencies(
groups=poetry_dep.groups, working_directory=working_directory
).direct_dependencies


def get_dependencies_from_latest_tag() -> dict[str, list[Package]]:
def get_dependencies_from_latest_tag() -> (
OrderedDict[str, dict[NormalizedPackageStr, Package]]
):
latest_tag = Git.get_latest_tag()
with tempfile.TemporaryDirectory() as path:
tmpdir = Path(path)
Expand Down
15 changes: 12 additions & 3 deletions exasol/toolbox/util/dependencies/shared_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

from typing import Annotated
from typing import (
Annotated,
NewType,
)

from packaging.version import Version
from pydantic import (
Expand All @@ -9,15 +12,21 @@
ConfigDict,
)

NormalizedPackageStr = NewType("NormalizedPackageStr", str)

VERSION_TYPE = Annotated[str, AfterValidator(lambda v: Version(v))]


def normalize_package_name(package_name: str) -> NormalizedPackageStr:
return NormalizedPackageStr(package_name.lower().replace("_", "-"))


class Package(BaseModel):
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

name: str
version: VERSION_TYPE

@property
def normalized_name(self) -> str:
return self.name.lower().replace("_", "-")
def normalized_name(self) -> NormalizedPackageStr:
return normalize_package_name(self.name)
Loading