Skip to content

Commit ccca69d

Browse files
authored
feat(poly create project): interactively suggest base and components to add (#362)
* feat: find bases not added in any project * feat: sort list of bases by closest matching name * refactor(sync): no need to pack and unpack brick data * feat: calculate needed bricks from a base * feat(create project): interactively add a base (and sync the components) to a new project * bump Poetry plugin to 1.40.0 * bump CLI to 1.33.0 * refactor: interactive added brick message logic
1 parent a195509 commit ccca69d

File tree

11 files changed

+157
-21
lines changed

11 files changed

+157
-21
lines changed

bases/polylith/cli/create.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def project_command(
5252
"""Creates a Polylith project."""
5353
create(name, description, _create_project)
5454

55+
project.interactive.run(name)
56+
5557

5658
@app.command("workspace")
5759
def workspace_command(

components/polylith/commands/sync.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
from pathlib import Path
22

3-
from polylith import info
43
from polylith import sync
54

65

76
def run(root: Path, ns: str, project_data: dict, options: dict):
87
is_quiet = options["quiet"]
98
is_verbose = options["verbose"]
109

11-
bases = info.get_bases(root, ns)
12-
components = info.get_components(root, ns)
13-
workspace_data = {"bases": bases, "components": components}
14-
15-
diff = sync.calculate_diff(root, ns, project_data, workspace_data)
10+
diff = sync.calculate_diff(root, ns, project_data)
1611

1712
sync.update_project(root, ns, diff)
1813

components/polylith/info/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from polylith.info.collect import (
2+
find_unused_bases,
23
get_bases,
34
get_bricks_in_projects,
45
get_components,
@@ -12,6 +13,7 @@
1213
)
1314

1415
__all__ = [
16+
"find_unused_bases",
1517
"get_bases",
1618
"get_bricks_in_projects",
1719
"get_components",

components/polylith/info/collect.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import difflib
12
from pathlib import Path
2-
from typing import List
3+
from typing import List, Set
34

45
from polylith.bricks import base, component
56
from polylith.project import get_packages_for_projects, parse_package_paths
@@ -53,3 +54,20 @@ def get_projects_data(root: Path, ns: str) -> List[dict]:
5354
components = get_components(root, ns)
5455

5556
return get_bricks_in_projects(root, components, bases, ns)
57+
58+
59+
def find_unused_bases(root: Path, ns: str) -> Set[str]:
60+
projects_data = get_projects_data(root, ns)
61+
62+
bases = get_bases(root, ns)
63+
bases_in_projects = set().union(*[p["bases"] for p in projects_data])
64+
65+
return set(bases).difference(bases_in_projects)
66+
67+
68+
def sort_bases_by_closest_match(bases: Set[str], name: str) -> List[str]:
69+
closest = difflib.get_close_matches(name, bases, cutoff=0.3)
70+
71+
rest = sorted([b for b in bases if b not in closest])
72+
73+
return closest + rest

components/polylith/poetry/commands/create_project.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,6 @@ def handle(self) -> int:
3737

3838
create(name, description, create_project)
3939

40+
project.interactive.run(name)
41+
4042
return 0

components/polylith/project/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from polylith.project import templates
1+
from polylith.project import interactive, templates
22
from polylith.project.create import create_project
33
from polylith.project.get import (
44
get_packages_for_projects,
@@ -14,6 +14,7 @@
1414
"get_project_name",
1515
"get_project_template",
1616
"get_toml",
17+
"interactive",
1718
"parse_package_paths",
1819
"templates",
1920
]
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from pathlib import Path
2+
from typing import List, Set
3+
4+
from polylith import configuration, info, repo, sync
5+
from polylith.reporting import theme
6+
from rich.console import Console
7+
from rich.padding import Padding
8+
from rich.prompt import Confirm, Prompt
9+
10+
console = Console(theme=theme.poly_theme)
11+
12+
13+
def create_added_brick_message(bricks: Set[str], tag: str, project_name: str) -> str:
14+
number_of_bricks = len(bricks)
15+
plural = "s" if number_of_bricks > 1 else ""
16+
17+
if tag == "base":
18+
grammar = f"base{plural}"
19+
else:
20+
grammar = f"component{plural}"
21+
22+
return f"[data]Added {number_of_bricks} [{tag}]{grammar}[/] to the [proj]{project_name}[/] project.[/]"
23+
24+
25+
def confirmation(diff: dict, project_name: str) -> None:
26+
pad = (1, 0, 0, 0)
27+
28+
if not diff:
29+
nothing_added_message = f"[data]No bricks added to [proj]{project_name}[/][/]."
30+
console.print(Padding(nothing_added_message, pad))
31+
32+
return
33+
34+
bases = diff["bases"]
35+
components = diff["components"]
36+
37+
bases_message = create_added_brick_message(bases, "base", project_name)
38+
console.print(Padding(bases_message, pad))
39+
40+
if len(components) == 0:
41+
return
42+
43+
components_message = create_added_brick_message(components, "comp", project_name)
44+
console.print(components_message)
45+
46+
47+
def add_bricks_to_project(
48+
root: Path,
49+
ns: str,
50+
project_name: str,
51+
possible_bases: List[str],
52+
) -> None:
53+
projects_data = info.get_projects_data(root, ns)
54+
project_data = next((p for p in projects_data if p["name"] == project_name), None)
55+
56+
if not project_data:
57+
return
58+
59+
message = f"[data]Project [proj]{project_name}[/] created.[/]"
60+
console.print(Padding(message, (0, 0, 1, 0)))
61+
62+
first, *_ = possible_bases
63+
64+
if not Confirm.ask(
65+
prompt=f"[data]Do you want to add bricks to the [proj]{project_name}[/] project?[/]",
66+
console=console,
67+
):
68+
return
69+
70+
question = "[data]What's the name of the Polylith [base]base[/] to add?[/]"
71+
72+
base = Prompt.ask(
73+
prompt=question,
74+
console=console,
75+
default=first,
76+
show_default=True,
77+
case_sensitive=False,
78+
)
79+
80+
all_bases = info.get_bases(root, ns)
81+
found_base = next((b for b in all_bases if str.lower(b) == str.lower(base)), None)
82+
83+
if not found_base:
84+
confirmation({}, project_name)
85+
return
86+
87+
diff = sync.calculate_needed_bricks(root, ns, project_data, found_base)
88+
89+
sync.update_project(root, ns, diff)
90+
91+
confirmation(diff, project_name)
92+
93+
94+
def run(project_name: str) -> None:
95+
root = repo.get_workspace_root(Path.cwd())
96+
ns = configuration.get_namespace_from_config(root)
97+
98+
possible_bases = sorted(info.find_unused_bases(root, ns))
99+
100+
if not possible_bases:
101+
return
102+
103+
add_bricks_to_project(root, ns, project_name, possible_bases)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from polylith.sync import report
2-
from polylith.sync.collect import calculate_diff
2+
from polylith.sync.collect import calculate_diff, calculate_needed_bricks
33
from polylith.sync.update import update_project
44

5-
__all__ = ["report", "calculate_diff", "update_project"]
5+
__all__ = ["report", "calculate_diff", "calculate_needed_bricks", "update_project"]

components/polylith/sync/collect.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
from pathlib import Path
2+
from typing import Set
23

34
from polylith import check, deps, info
45

56

6-
def calculate_diff(
7-
root: Path,
8-
namespace: str,
9-
project_data: dict,
10-
workspace_data: dict,
11-
) -> dict:
12-
bases = set(project_data["bases"])
7+
def _calculate(root: Path, namespace: str, project_data: dict, bases: Set[str]) -> dict:
138
components = set(project_data["components"])
149

15-
all_bases = workspace_data["bases"]
16-
all_components = workspace_data["components"]
10+
all_bases = info.get_bases(root, namespace)
11+
all_components = info.get_components(root, namespace)
1712

1813
brick_imports = deps.get_brick_imports(root, namespace, bases, components)
1914
is_project = info.is_project(project_data)
@@ -35,3 +30,21 @@ def calculate_diff(
3530
"components": components_diff,
3631
"brick_imports": brick_imports,
3732
}
33+
34+
35+
def calculate_diff(root: Path, namespace: str, project_data: dict) -> dict:
36+
bases = set(project_data["bases"])
37+
38+
return _calculate(root, namespace, project_data, bases)
39+
40+
41+
def calculate_needed_bricks(
42+
root: Path, namespace: str, project_data: dict, base: str
43+
) -> dict:
44+
bases = {base}
45+
46+
res = _calculate(root, namespace, project_data, bases)
47+
48+
needed_bases = res["bases"].union(bases)
49+
50+
return {**res, **{"bases": needed_bases}}

projects/poetry_polylith_plugin/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "poetry-polylith-plugin"
3-
version = "1.39.0"
3+
version = "1.40.0"
44
description = "A Poetry plugin that adds tooling support for the Polylith Architecture"
55
authors = ["David Vujic"]
66
homepage = "https://davidvujic.github.io/python-polylith-docs/"

0 commit comments

Comments
 (0)