Skip to content

Commit b4f9094

Browse files
ArBridgemanckunki
andauthored
Feature/382 update release prepare to add on dependency changes (#505)
* Remove pylint addition which is included already in lint:code * Remove unused secrets.GITHUB_TOKEN from checks.yml * Add tracking types for DependencyChange with tests Co-authored-by: Christoph Kuhnke <christoph.kuhnke@exasol.com> * Switch dependencies from list to dict of packages for O(1) retrieval * Update licenses to work with dictionary and have shared special string type for better readability * Switch to class for cleaner look than function in function * Add dependency updates to changelog * Ignore typing as if-elif safeguard None from getting to those lines * Add changelog entry * Pass on OrderedDict type * Pass on OrderedDict type * Restore order to licenses as dict keys to set loses order * Fix indent and make date variable; could alternately add freeze gun or mock datetime * Add types for track_changes.py * Rename old to previous for dependencies * Use group to be consistent with what we use in dependency changes and pyproject.toml * Remove comment as not needed or useful * Switch source_path to be correctly named root_path * Add output types to methods * Simplify output for _format_table_row * Simplify output for _format_group_table_header * Use normalized name from object instead of import * Switch from usage of string with += to using append in a list and joining * Switch from old_version to previous_version * Switch from old_package to previous_package * Switch from str to NormalizedPackageStr as that's what the dict keys are * Switch Union[*, None] with Optional[*] * Add comment to make clearer what the operation does * Add on description of Changelogs arguments * Improve terminology & adapt _describe_dependency_changes to be simpler * Add deterministic sorting to versioned changes file * Simplify _create_versioned_changelog and fix moved type * Remove unneeded, second cleancode() usage * Switch text from changes to contents --------- Co-authored-by: Christoph Kuhnke <christoph.kuhnke@exasol.com>
1 parent d81ea91 commit b4f9094

File tree

20 files changed

+675
-225
lines changed

20 files changed

+675
-225
lines changed

.github/workflows/checks.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,6 @@ jobs:
168168
runs-on: ubuntu-24.04
169169
permissions:
170170
contents: read
171-
env:
172-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
173171
strategy:
174172
fail-fast: false
175173
matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }}

doc/changes/unreleased.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Unreleased
22

3+
## Feature
4+
5+
* #382: Added onto nox session `release:prepare` to append dependency changes between current and latest tag
6+
37
## Refactoring
48

59
* #498: Centralized changelog code relevant for `release:trigger` & robustly tested

exasol/toolbox/nox/_artifacts.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
import sys
88
from collections.abc import Iterable
99
from pathlib import Path
10-
from typing import (
11-
Optional,
12-
Union,
13-
)
10+
from typing import Optional
1411

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

189186

190187
def _prepare_coverage_xml(
191-
session: Session, source: Path, cwd: Union[Path, None] = None
188+
session: Session, source: Path, cwd: Optional[Path] = None
192189
) -> None:
193190
"""
194191
Prepare the coverage XML for input into Sonar

exasol/toolbox/nox/_dependencies.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from nox import Session
1010

1111
from exasol.toolbox.util.dependencies.licenses import (
12-
licenses,
13-
packages_to_markdown,
12+
PackageLicenseReport,
13+
get_licenses,
1414
)
1515
from exasol.toolbox.util.dependencies.poetry_dependencies import get_dependencies
1616

@@ -85,8 +85,11 @@ def run(self, session: Session) -> None:
8585
def dependency_licenses(session: Session) -> None:
8686
"""Return the packages with their licenses"""
8787
dependencies = get_dependencies(working_directory=Path())
88-
package_infos = licenses()
89-
print(packages_to_markdown(dependencies=dependencies, packages=package_infos))
88+
licenses = get_licenses()
89+
license_markdown = PackageLicenseReport(
90+
dependencies=dependencies, licenses=licenses
91+
)
92+
print(license_markdown.to_markdown())
9093

9194

9295
@nox.session(name="dependency:audit", python=False)

exasol/toolbox/nox/_release.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ def prepare_release(session: Session) -> None:
119119
_ = _update_project_version(session, new_version)
120120

121121
changelogs = Changelogs(
122-
changes_path=PROJECT_CONFIG.doc / "changes", version=new_version
122+
changes_path=PROJECT_CONFIG.doc / "changes",
123+
root_path=PROJECT_CONFIG.root,
124+
version=new_version,
123125
)
124126
changelogs.update_changelogs_for_release()
125127
changed_files = changelogs.get_changed_files()

exasol/toolbox/templates/github/workflows/checks.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,6 @@ jobs:
166166
runs-on: ubuntu-24.04
167167
permissions:
168168
contents: read
169-
env:
170-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
171169
strategy:
172170
fail-fast: false
173171
matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }}

exasol/toolbox/util/dependencies/licenses.py

Lines changed: 63 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
import subprocess
44
import tempfile
5+
from collections import OrderedDict
6+
from dataclasses import dataclass
57
from inspect import cleandoc
68
from json import loads
79
from typing import Optional
810

911
from pydantic import field_validator
1012

11-
from exasol.toolbox.util.dependencies.shared_models import Package
13+
from exasol.toolbox.util.dependencies.shared_models import (
14+
NormalizedPackageStr,
15+
Package,
16+
)
1217

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

9095

91-
def _packages_from_json(json: str) -> list[PackageLicense]:
96+
def _packages_from_json(json: str) -> dict[NormalizedPackageStr, PackageLicense]:
9297
packages = loads(json)
93-
return [
94-
PackageLicense(
95-
name=package["Name"],
96-
package_link=package["URL"],
97-
version=package["Version"],
98-
license=package["License"],
99-
)
98+
return {
99+
package_license.normalized_name: package_license
100100
for package in packages
101-
]
101+
if (
102+
package_license := PackageLicense(
103+
name=package["Name"],
104+
package_link=package["URL"],
105+
version=package["Version"],
106+
license=package["License"],
107+
)
108+
)
109+
}
102110

103111

104-
def licenses() -> list[PackageLicense]:
112+
def get_licenses() -> dict[NormalizedPackageStr, PackageLicense]:
105113
with tempfile.NamedTemporaryFile() as file:
106114
subprocess.run(
107115
[
@@ -117,62 +125,51 @@ def licenses() -> list[PackageLicense]:
117125
return _packages_from_json(file.read().decode())
118126

119127

120-
def packages_to_markdown(
121-
dependencies: dict[str, list], packages: list[PackageLicense]
122-
) -> str:
123-
def heading():
124-
return "# Dependencies\n"
128+
@dataclass(frozen=True)
129+
class PackageLicenseReport:
130+
dependencies: OrderedDict[str, dict[NormalizedPackageStr, Package]]
131+
licenses: dict[NormalizedPackageStr, PackageLicense]
125132

126-
def dependency(
127-
group: str,
128-
group_packages: list[Package],
129-
packages: list[PackageLicense],
130-
) -> str:
131-
def _header(_group: str):
132-
_group = "".join([word.capitalize() for word in _group.strip().split()])
133-
text = f"## {_group} Dependencies\n"
134-
text += "|Package|Version|License|\n"
135-
text += "|---|---|---|\n"
136-
return text
137-
138-
def _rows(
139-
_group_packages: list[Package], _packages: list[PackageLicense]
140-
) -> str:
141-
text = ""
142-
for package in _group_packages:
143-
consistent = filter(
144-
lambda elem: elem.normalized_name == package.normalized_name,
145-
_packages,
146-
)
147-
for content in consistent:
148-
if content.package_link:
149-
text += f"|[{content.name}]({content.package_link})"
150-
else:
151-
text += f"|{content.name}"
152-
text += f"|{content.version}"
153-
if content.license_link:
154-
text += f"|[{content.license}]({content.license_link})|\n"
155-
else:
156-
text += f"|{content.license}|\n"
157-
text += "\n"
158-
return text
159-
160-
_template = cleandoc(
133+
@staticmethod
134+
def _format_group_table_header(group: str) -> str:
135+
return cleandoc(
136+
f"""## `{group}` Dependencies
137+
|Package|Version|License|
138+
|---|---|---|
161139
"""
162-
{header}{rows}
163-
"""
164140
)
165-
return _template.format(
166-
header=_header(group), rows=_rows(group_packages, packages)
167-
)
168-
169-
template = cleandoc(
170-
"""
171-
{heading}{rows}
172-
"""
173-
)
174141

175-
rows = ""
176-
for group in dependencies:
177-
rows += dependency(group, dependencies[group], packages)
178-
return template.format(heading=heading(), rows=rows)
142+
def _format_group_table(
143+
self, group: str, group_package_names: set[NormalizedPackageStr]
144+
) -> str:
145+
group_header = self._format_group_table_header(group=group)
146+
147+
rows = []
148+
for package_name in sorted(group_package_names):
149+
if license_info := self.licenses.get(package_name):
150+
rows.append(self._format_table_row(license_info=license_info))
151+
152+
return f"""{group_header}\n{''.join(rows)}\n"""
153+
154+
@staticmethod
155+
def _format_table_row(license_info: PackageLicense) -> str:
156+
row_package = f"{license_info.name}"
157+
if license_info.package_link:
158+
row_package = f"[{license_info.name}]({license_info.package_link})"
159+
160+
row_license = f"{license_info.license}"
161+
if license_info.license_link:
162+
row_license = f"[{license_info.license}]({license_info.license_link})"
163+
164+
return f"|{row_package}|{license_info.version}|{row_license}|\n"
165+
166+
def to_markdown(self) -> str:
167+
rows = []
168+
for group in self.dependencies:
169+
group_package_names = set(self.dependencies[group].keys())
170+
rows.append(
171+
self._format_group_table(
172+
group=group, group_package_names=group_package_names
173+
)
174+
)
175+
return cleandoc(f"""# Dependencies\n\n{''.join(rows)}""")

exasol/toolbox/util/dependencies/poetry_dependencies.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import subprocess
44
import tempfile
5+
from collections import OrderedDict
56
from pathlib import Path
67
from typing import Optional
78

@@ -12,7 +13,10 @@
1213
)
1314
from tomlkit import TOMLDocument
1415

15-
from exasol.toolbox.util.dependencies.shared_models import Package
16+
from exasol.toolbox.util.dependencies.shared_models import (
17+
NormalizedPackageStr,
18+
Package,
19+
)
1620
from exasol.toolbox.util.git import Git
1721

1822

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

95-
def _extract_from_poetry_show(self, output_text: str) -> list[Package]:
96-
return [
97-
package
99+
def _extract_from_poetry_show(
100+
self, output_text: str
101+
) -> dict[NormalizedPackageStr, Package]:
102+
return {
103+
package.normalized_name: package
98104
for line in output_text.splitlines()
99105
if (package := self._extract_from_line(line))
100-
]
106+
}
101107

102108
@property
103-
def direct_dependencies(self) -> dict[str, list[Package]]:
104-
dependencies = {}
109+
def direct_dependencies(
110+
self,
111+
) -> OrderedDict[str, dict[NormalizedPackageStr, Package]]:
112+
dependencies = OrderedDict()
105113
for group in self.groups:
106114
command = (
107115
"poetry",
@@ -122,7 +130,7 @@ def direct_dependencies(self) -> dict[str, list[Package]]:
122130
return dependencies
123131

124132
@property
125-
def all_dependencies(self) -> dict[str, list[Package]]:
133+
def all_dependencies(self) -> OrderedDict[str, dict[NormalizedPackageStr, Package]]:
126134
command = ("poetry", "show", "--no-truncate")
127135
output = subprocess.run(
128136
command,
@@ -133,28 +141,33 @@ def all_dependencies(self) -> dict[str, list[Package]]:
133141
)
134142

135143
direct_dependencies = self.direct_dependencies.copy()
136-
transitive_dependencies = []
144+
145+
transitive_dependencies = {}
137146
names_direct_dependencies = {
138-
dep.name
139-
for group_list in direct_dependencies.values()
140-
for dep in group_list
147+
package_name
148+
for group_list in direct_dependencies
149+
for package_name in group_list
141150
}
142151
for line in output.stdout.splitlines():
143152
dep = self._extract_from_line(line=line)
144153
if dep and dep.name not in names_direct_dependencies:
145-
transitive_dependencies.append(dep)
154+
transitive_dependencies[dep.normalized_name] = dep
146155

147156
return direct_dependencies | {TRANSITIVE_GROUP.name: transitive_dependencies}
148157

149158

150-
def get_dependencies(working_directory: Path) -> dict[str, list[Package]]:
159+
def get_dependencies(
160+
working_directory: Path,
161+
) -> OrderedDict[str, dict[NormalizedPackageStr, Package]]:
151162
poetry_dep = PoetryToml.load_from_toml(working_directory=working_directory)
152163
return PoetryDependencies(
153164
groups=poetry_dep.groups, working_directory=working_directory
154165
).direct_dependencies
155166

156167

157-
def get_dependencies_from_latest_tag() -> dict[str, list[Package]]:
168+
def get_dependencies_from_latest_tag() -> (
169+
OrderedDict[str, dict[NormalizedPackageStr, Package]]
170+
):
158171
latest_tag = Git.get_latest_tag()
159172
with tempfile.TemporaryDirectory() as path:
160173
tmpdir = Path(path)
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import annotations
22

3-
from typing import Annotated
3+
from typing import (
4+
Annotated,
5+
NewType,
6+
)
47

58
from packaging.version import Version
69
from pydantic import (
@@ -9,15 +12,21 @@
912
ConfigDict,
1013
)
1114

15+
NormalizedPackageStr = NewType("NormalizedPackageStr", str)
16+
1217
VERSION_TYPE = Annotated[str, AfterValidator(lambda v: Version(v))]
1318

1419

20+
def normalize_package_name(package_name: str) -> NormalizedPackageStr:
21+
return NormalizedPackageStr(package_name.lower().replace("_", "-"))
22+
23+
1524
class Package(BaseModel):
1625
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
1726

1827
name: str
1928
version: VERSION_TYPE
2029

2130
@property
22-
def normalized_name(self) -> str:
23-
return self.name.lower().replace("_", "-")
31+
def normalized_name(self) -> NormalizedPackageStr:
32+
return normalize_package_name(self.name)

0 commit comments

Comments
 (0)