Skip to content

Commit 2a50de1

Browse files
committed
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
1 parent 55ac847 commit 2a50de1

File tree

5 files changed

+248
-171
lines changed

5 files changed

+248
-171
lines changed

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/util/dependencies/licenses.py

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

33
import subprocess
44
import tempfile
5+
from dataclasses import dataclass
56
from inspect import cleandoc
67
from json import loads
78
from typing import Optional
89

910
from pydantic import field_validator
1011

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

1318
LICENSE_MAPPING_TO_ABBREVIATION = {
1419
"BSD License": "BSD",
@@ -88,20 +93,20 @@ 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(
98+
return {
99+
normalize_package_name(package["Name"]): PackageLicense(
95100
name=package["Name"],
96101
package_link=package["URL"],
97102
version=package["Version"],
98103
license=package["License"],
99104
)
100105
for package in packages
101-
]
106+
}
102107

103108

104-
def licenses() -> list[PackageLicense]:
109+
def get_licenses() -> dict[NormalizedPackageStr, PackageLicense]:
105110
with tempfile.NamedTemporaryFile() as file:
106111
subprocess.run(
107112
[
@@ -117,62 +122,56 @@ def licenses() -> list[PackageLicense]:
117122
return _packages_from_json(file.read().decode())
118123

119124

120-
def packages_to_markdown(
121-
dependencies: dict[str, list], packages: list[PackageLicense]
122-
) -> str:
123-
def heading():
124-
return "# Dependencies\n"
125-
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(
161-
"""
162-
{header}{rows}
163-
"""
164-
)
165-
return _template.format(
166-
header=_header(group), rows=_rows(group_packages, packages)
167-
)
168-
169-
template = cleandoc(
170-
"""
171-
{heading}{rows}
172-
"""
173-
)
174-
175-
rows = ""
176-
for group in dependencies:
177-
rows += dependency(group, dependencies[group], packages)
178-
return template.format(heading=heading(), rows=rows)
125+
@dataclass(frozen=True)
126+
class PackageLicenseReport:
127+
dependencies: dict[str, dict[NormalizedPackageStr, Package]]
128+
licenses: dict[NormalizedPackageStr, PackageLicense]
129+
130+
@staticmethod
131+
def _format_group_table_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 _format_group_table(
139+
self, group: str, group_package_names: set[NormalizedPackageStr]
140+
):
141+
group_header = self._format_group_table_header(group=group)
142+
143+
rows_text = ""
144+
for package_name in group_package_names:
145+
if license_info := self.licenses.get(package_name):
146+
rows_text += self._format_table_row(license_info=license_info)
147+
148+
return f"""{group_header}{rows_text}\n"""
149+
150+
@staticmethod
151+
def _format_table_row(license_info: PackageLicense) -> str:
152+
text = ""
153+
# column: package
154+
if license_info.package_link:
155+
text += f"|[{license_info.name}]({license_info.package_link})"
156+
else:
157+
text += f"|{license_info.name}"
158+
159+
# column: version
160+
text += f"|{license_info.version}"
161+
162+
# column: license
163+
if license_info.license_link:
164+
text += f"|[{license_info.license}]({license_info.license_link})|"
165+
else:
166+
text += f"|{license_info.license}|"
167+
168+
return text + "\n"
169+
170+
def to_markdown(self) -> str:
171+
rows = ""
172+
for group in self.dependencies:
173+
group_package_names = set(self.dependencies[group].keys())
174+
rows += self._format_group_table(
175+
group=group, group_package_names=group_package_names
176+
)
177+
return cleandoc(f"""# Dependencies\n\n{rows}""")

exasol/toolbox/util/dependencies/poetry_dependencies.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
)
1313
from tomlkit import TOMLDocument
1414

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

1821

@@ -92,15 +95,17 @@ def _extract_from_line(line: str) -> Optional[Package]:
9295
return None
9396
return Package(name=split_line[0], version=split_line[1])
9497

95-
def _extract_from_poetry_show(self, output_text: str) -> dict[str, Package]:
98+
def _extract_from_poetry_show(
99+
self, output_text: str
100+
) -> dict[NormalizedPackageStr, Package]:
96101
return {
97-
package.name: package
102+
package.normalized_name: package
98103
for line in output_text.splitlines()
99104
if (package := self._extract_from_line(line))
100105
}
101106

102107
@property
103-
def direct_dependencies(self) -> dict[str, dict[str, Package]]:
108+
def direct_dependencies(self) -> dict[str, dict[NormalizedPackageStr, Package]]:
104109
dependencies = {}
105110
for group in self.groups:
106111
command = (
@@ -122,7 +127,7 @@ def direct_dependencies(self) -> dict[str, dict[str, Package]]:
122127
return dependencies
123128

124129
@property
125-
def all_dependencies(self) -> dict[str, dict[str, Package]]:
130+
def all_dependencies(self) -> dict[str, dict[NormalizedPackageStr, Package]]:
126131
command = ("poetry", "show", "--no-truncate")
127132
output = subprocess.run(
128133
command,
@@ -143,19 +148,23 @@ def all_dependencies(self) -> dict[str, dict[str, Package]]:
143148
for line in output.stdout.splitlines():
144149
dep = self._extract_from_line(line=line)
145150
if dep and dep.name not in names_direct_dependencies:
146-
transitive_dependencies[dep.name] = dep
151+
transitive_dependencies[dep.normalized_name] = dep
147152

148153
return direct_dependencies | {TRANSITIVE_GROUP.name: transitive_dependencies}
149154

150155

151-
def get_dependencies(working_directory: Path) -> dict[str, list[Package]]:
156+
def get_dependencies(
157+
working_directory: Path,
158+
) -> dict[str, dict[NormalizedPackageStr, Package]]:
152159
poetry_dep = PoetryToml.load_from_toml(working_directory=working_directory)
153160
return PoetryDependencies(
154161
groups=poetry_dep.groups, working_directory=working_directory
155162
).direct_dependencies
156163

157164

158-
def get_dependencies_from_latest_tag() -> dict[str, list[Package]]:
165+
def get_dependencies_from_latest_tag() -> (
166+
dict[str, dict[NormalizedPackageStr, Package]]
167+
):
159168
latest_tag = Git.get_latest_tag()
160169
with tempfile.TemporaryDirectory() as path:
161170
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)