Skip to content

Commit

Permalink
Parse third party deps (#186)
Browse files Browse the repository at this point in the history
* feat(toml): parse deps from pyproject.toml

* fix(toml): typo in fallback when getting third-party dependencies

* feat(poly libs): compare project dependencies

* feat(poly libs): shorten libs summary output

* feat(poly libs): highlight version diffs

* feat(poly libs): redesign the output from the command

* bump minor versions for all projects

* bump dev dependency (black)
  • Loading branch information
DavidVujic committed Apr 8, 2024
1 parent 0752b77 commit d57c8e3
Show file tree
Hide file tree
Showing 17 changed files with 597 additions and 463 deletions.
5 changes: 4 additions & 1 deletion bases/polylith/cli/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def libs_command(
strict: Annotated[bool, options.strict] = False,
directory: Annotated[str, options.directory] = "",
alias: Annotated[str, options.alias] = "",
short: Annotated[bool, options.short] = False,
):
"""Show third-party libraries used in the workspace."""
root = repo.get_workspace_root(Path.cwd())
Expand All @@ -84,11 +85,13 @@ def libs_command(
cli_options = {
"strict": strict,
"alias": str.split(alias, ",") if alias else [],
"short": short,
}

projects_data = filtered_projects_data(all_projects_data, directory)

results = {commands.libs.run(root, ns, p, cli_options) for p in projects_data}
results = commands.libs.run(root, ns, projects_data, cli_options)
commands.libs.run_library_versions(projects_data, all_projects_data, cli_options)

if not all(results):
raise Exit(code=1)
Expand Down
4 changes: 3 additions & 1 deletion components/polylith/check/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def diff(known_bricks: Set[str], bases: Set[str], components: Set[str]) -> Set[s
return known_bricks.difference(bricks)


def imports_diff(brick_imports: dict, bases: Set[str], components: Set[str]) -> Set[str]:
def imports_diff(
brick_imports: dict, bases: Set[str], components: Set[str]
) -> Set[str]:
flattened_bases = set().union(*brick_imports["bases"].values())
flattened_components = set().union(*brick_imports["components"].values())

Expand Down
46 changes: 41 additions & 5 deletions components/polylith/commands/libs.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
from functools import reduce
from pathlib import Path
from typing import List, Set

from polylith import distributions
from polylith.libs import report


def run(root: Path, ns: str, project_data: dict, options: dict) -> bool:
def missing_libs(project_data: dict, imports: dict, options: dict) -> bool:
is_strict = options["strict"]
library_alias = options["alias"]

name = project_data["name"]
deps = project_data["deps"]

brick_imports = report.get_third_party_imports(root, ns, project_data)

report.print_libs_summary(brick_imports, project_data)
report.print_libs_in_bricks(brick_imports)
brick_imports = imports[name]

libs = distributions.known_aliases_and_sub_dependencies(deps, library_alias)

Expand All @@ -24,3 +23,40 @@ def run(root: Path, ns: str, project_data: dict, options: dict) -> bool:
name,
is_strict,
)


def flatten_imports(acc: dict, item: dict) -> dict:
bases = item.get("bases", {})
components = item.get("components", {})

return {
"bases": {**acc.get("bases", {}), **bases},
"components": {**acc.get("components", {}), **components},
}


def run_library_versions(
projects_data: List[dict], all_projects_data: List[dict], options: dict
) -> None:
development_data = next(p for p in all_projects_data if p["type"] == "development")
filtered_projects_data = [p for p in projects_data if p["type"] != "development"]

report.print_libs_in_projects(development_data, filtered_projects_data, options)


def run(
root: Path,
ns: str,
projects_data: List[dict],
options: dict,
) -> Set[bool]:
imports = {
p["name"]: report.get_third_party_imports(root, ns, p) for p in projects_data
}

flattened: dict = reduce(flatten_imports, imports.values(), {})

report.print_libs_summary()
report.print_libs_in_bricks(flattened)

return {missing_libs(p, imports, options) for p in projects_data}
6 changes: 2 additions & 4 deletions components/polylith/distributions/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@
)


def known_aliases_and_sub_dependencies(
deps: dict, library_alias: list
) -> Set[str]:
def known_aliases_and_sub_dependencies(deps: dict, library_alias: list) -> Set[str]:
"""Collect known aliases (packages) for third-party libraries.
When the library origin is not from a lock-file:
collect sub-dependencies for each library, and append to the result.
"""

third_party_libs = deps["items"]
third_party_libs = {k for k, _v in deps["items"].items()}
lock_file = str.endswith(deps["source"], ".lock")

dists = list(importlib.metadata.distributions())
Expand Down
100 changes: 83 additions & 17 deletions components/polylith/libs/report.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import difflib
from operator import itemgetter
from pathlib import Path
from typing import Set
from typing import List, Set, Union

from polylith import info, workspace
from polylith import workspace
from polylith.libs import grouping
from polylith.reporting import theme
from rich import box
from rich import box, markup
from rich.console import Console
from rich.padding import Padding
from rich.table import Table
Expand Down Expand Up @@ -63,22 +63,10 @@ def calculate_diff(
return filter_close_matches(unknown_imports, deps, cutoff)


def print_libs_summary(brick_imports: dict, project_data: dict) -> None:
def print_libs_summary() -> None:
console = Console(theme=theme.poly_theme)

name = project_data["name"]
is_project = info.is_project(project_data)

printable_name = f"[proj]{name}[/]" if is_project else "[data]development[/]"
console.print(
Padding(f"[data]Library summary for [/]{printable_name}", (1, 0, 1, 0))
)

bases_len = len(flatten_imports(brick_imports, "bases"))
components_len = len(flatten_imports(brick_imports, "components"))

console.print(f"[comp]Libraries used in components[/]: [data]{components_len}[/]")
console.print(f"[base]Libraries used in bases[/]: [data]{bases_len}[/]")
console.print(Padding("[data]Libraries in bricks[/]", (1, 0, 0, 0)))


def print_libs_in_bricks(brick_imports: dict) -> None:
Expand Down Expand Up @@ -127,3 +115,81 @@ def print_missing_installed_libs(

console.print(f":thinking_face: {missing}")
return False


def printable_version(version: Union[str, None], is_same_version: bool) -> str:
ver = version or "-"
markup = "data" if is_same_version else "bold"

return f"[{markup}]{ver}[/]"


def get_version(lib: str, project_data: dict) -> str:
return project_data["deps"]["items"].get(lib)


def find_version(
lib: str, project_name: str, projects_data: List[dict]
) -> Union[str, None]:
project_data = next(p for p in projects_data if p["name"] == project_name)

return get_version(lib, project_data)


def printable_header(header: str, short: bool) -> str:
return "\n".join(header) if short else header


def is_same_version(versions: list) -> bool:
unique = set([v for v in versions if v])

return len(unique) == 1 if unique else True


def libs_in_projects_table(
development_data: dict,
projects_data: List[dict],
libraries: set,
options: dict,
) -> Table:
table = Table(box=box.SIMPLE_HEAD)

short = options["short"]

project_names = sorted({p["name"] for p in projects_data})
project_headers = [f"[proj]{printable_header(n, short)}[/]" for n in project_names]
dev_header = printable_header("development", short)
headers = ["[data]library[/]"] + project_headers + [f"[data]{dev_header}[/]"]

for header in headers:
table.add_column(header)

for lib in sorted(libraries):
proj_versions = [find_version(lib, n, projects_data) for n in project_names]
dev_version = get_version(lib, development_data)

is_same = is_same_version(proj_versions + [dev_version])
printable_proj_versions = [printable_version(v, is_same) for v in proj_versions]
printable_dev_version = printable_version(dev_version, is_same)

cols = [markup.escape(lib)] + printable_proj_versions + [printable_dev_version]

table.add_row(*cols)

return table


def print_libs_in_projects(
development_data: dict, projects_data: List[dict], options: dict
) -> None:
flattened = {k for proj in projects_data for k, _v in proj["deps"]["items"].items()}

if not flattened:
return

table = libs_in_projects_table(development_data, projects_data, flattened, options)

console = Console(theme=theme.poly_theme)

console.print(Padding("[data]Library versions in projects[/]", (1, 0, 0, 0)))
console.print(table, overflow="ellipsis")
33 changes: 24 additions & 9 deletions components/polylith/poetry/commands/libs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pathlib import Path

from cleo.helpers import option
from poetry.console.commands.command import Command
from polylith import commands, configuration, info, repo
from polylith.poetry.commands.check import command_options
Expand All @@ -10,34 +11,48 @@ class LibsCommand(Command):
name = "poly libs"
description = "Show third-party libraries used in the workspace."

options = command_options
options = command_options + [
option(
long_name="short",
short_name="s",
description="Print short view",
flag=True,
),
]

def print_report(self, root: Path, ns: str, data: dict) -> bool:
def merged_project_data(self, data: dict) -> dict:
name = data["name"]
path = data["path"]

options = {"strict": self.option("strict"), "alias": self.option("alias")}

try:
third_party_libs = find_third_party_libs(self.poetry, path)
merged = {
return {
**data,
**{"deps": {"items": third_party_libs, "source": "poetry.lock"}},
}

return commands.libs.run(root, ns, merged, options)
except ValueError as e:
self.line_error(f"{name}: <error>{e}</error>")
return False
return data

def handle(self) -> int:
options = {
"strict": self.option("strict"),
"alias": self.option("alias"),
"short": self.option("short"),
}

directory = self.option("directory")
root = repo.get_workspace_root(Path.cwd())
ns = configuration.get_namespace_from_config(root)

all_projects_data = info.get_projects_data(root, ns)
projects_data = filter_projects_data(self.poetry, directory, all_projects_data)

results = {self.print_report(root, ns, data) for data in projects_data}
merged_projects_data = [
self.merged_project_data(data) for data in projects_data
]

results = commands.libs.run(root, ns, merged_projects_data, options)
commands.libs.run_library_versions(projects_data, all_projects_data, options)

return 0 if all(results) else 1
6 changes: 3 additions & 3 deletions components/polylith/poetry/internals.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Iterable, List, Set, Union
from typing import Iterable, List, Union

from poetry.factory import Factory
from poetry.poetry import Poetry
Expand All @@ -19,15 +19,15 @@ def distributions(poetry: Poetry, path: Union[Path, None]) -> Iterable:
return env.site_packages.distributions()


def find_third_party_libs(poetry: Poetry, path: Union[Path, None]) -> Set:
def find_third_party_libs(poetry: Poetry, path: Union[Path, None]) -> dict:
project_poetry = get_project_poetry(poetry, path)

if not project_poetry.locker.is_locked():
raise ValueError("poetry.lock not found. Run `poetry lock` to create it.")

packages = project_poetry.locker.locked_repository().packages

return {p.name for p in packages}
return {p.name: str(p.version) for p in packages}


def filter_projects_data(
Expand Down
2 changes: 2 additions & 0 deletions components/polylith/toml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
get_project_dependencies,
get_project_package_includes,
get_project_packages_from_polylith_section,
parse_project_dependencies,
read_toml_document,
)

Expand All @@ -11,5 +12,6 @@
"get_project_dependencies",
"get_project_package_includes",
"get_project_packages_from_polylith_section",
"parse_project_dependencies",
"read_toml_document",
]

0 comments on commit d57c8e3

Please sign in to comment.