Skip to content

Commit d74ea0e

Browse files
author
phernandez
committed
refactor: restructure component files and update dependencies
Moved component files to new directories for better organization. Updated pyproject.toml and uv.lock reflecting changes and added tomli-w dependency. Enhanced CLI tool with dependency handling and a dry run feature.
1 parent b6e4fc8 commit d74ea0e

File tree

5 files changed

+186
-72
lines changed

5 files changed

+186
-72
lines changed

basic_components/cli.py

Lines changed: 136 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from pathlib import Path
2+
from typing import List, Dict, Set
3+
import tomli
4+
import importlib.resources
25

36
import copier
47
import typer
58
from rich.console import Console
69
from rich.panel import Panel
10+
from rich.tree import Tree
711

812
app = typer.Typer(
913
name="components",
@@ -18,73 +22,173 @@
1822
DEFAULT_COMPONENTS_DIR = Path("components/ui")
1923
DEFAULT_BRANCH = "main"
2024

21-
# typer cli arg options
22-
COMPONENTS_DIR_OPTION = typer.Option(DEFAULT_COMPONENTS_DIR, "--components-dir", "-d", help="Directory to update components in")
23-
REPO_URL_OPTION = typer.Option(DEFAULT_REPO_URL, "--repo-url", "-r", help="Repository URL to update from")
24-
BRANCH_OPTION = typer.Option(DEFAULT_BRANCH, "--branch", "-b", help="Branch, tag, or commit to update from")
25-
25+
def load_dependencies() -> Dict[str, List[str]]:
26+
"""Load component dependencies from component_dependencies.toml within the package."""
27+
try:
28+
with importlib.resources.open_text('basic_components', 'component_dependencies.toml') as f:
29+
toml_data = tomli.loads(f.read())
30+
return toml_data.get('dependencies', {})
31+
except Exception as e:
32+
console.print(f"[red]Error loading dependencies: {e}[/red]")
33+
return {}
34+
35+
def normalize_component_name(name: str) -> str:
36+
"""Convert component references to normalized form"""
37+
# Only normalize if it's an icon reference
38+
if name.startswith("icons/"):
39+
# If already in icons/Name format, return as-is
40+
return name
41+
42+
if "/" not in name and name != "icons":
43+
# Handle bare icon names (without icons/ prefix)
44+
# Convert to PascalCase if needed
45+
if name.lower() in {"check", "x", "moon", "sun"}:
46+
return f"icons/{name.title()}"
47+
# Handle compound names
48+
if name.lower() in {"chevron-right", "chevron-down", "chevron-up", "chevrons-up-down"}:
49+
parts = name.split("-")
50+
pascal_name = "".join(p.title() for p in parts)
51+
return f"icons/{pascal_name}"
52+
53+
return name
54+
55+
def get_component_pattern(component: str) -> str:
56+
"""Get the file pattern for a component."""
57+
if component.startswith("icons/"):
58+
icon_name = component.split("/")[1]
59+
return f"icons/{icon_name}Icon.jinja"
60+
else:
61+
return f"{component}/**"
2662

2763
def add_component(
28-
component: str,
29-
dest_dir: Path,
30-
repo_url: str = DEFAULT_REPO_URL,
31-
branch: str = DEFAULT_BRANCH,
64+
component: str,
65+
dest_dir: Path,
66+
repo_url: str = DEFAULT_REPO_URL,
67+
branch: str = DEFAULT_BRANCH,
68+
dry_run: bool = False,
3269
) -> None:
3370
"""Add a specific component to the project."""
3471
try:
35-
console.print(f"[green]Installing {component} from '{repo_url}' ...[/green]")
72+
console.print(f"[green]Installing {component}...[/green]")
73+
74+
# Get the pattern for this component
75+
pattern = get_component_pattern(component)
76+
77+
# Build exclude list - exclude everything except our pattern
78+
excludes = ["*", f"!{pattern}"]
79+
80+
# Debug output
81+
console.print("[yellow]Debug: Copying with args:[/yellow]")
82+
console.print(f" src_path: {repo_url}")
83+
console.print(f" dst_path: {dest_dir}")
84+
console.print(f" exclude patterns: {excludes}")
85+
console.print(f" vcs_ref: {branch}")
3686

3787
copier.run_copy(
3888
src_path=repo_url,
3989
dst_path=str(dest_dir),
40-
exclude=[
41-
"*",
42-
f"!{component}",
43-
],
90+
exclude=excludes,
4491
vcs_ref=branch,
92+
pretend=dry_run,
4593
)
4694

47-
except Exception as e: # pyright: ignore [reportAttributeAccessIssue]
48-
console.print(f"[red]Error: {str(e)}[/red]")
95+
except Exception as e:
96+
console.print(f"[red]Error installing {component}: {str(e)}[/red]")
4997
raise typer.Exit(1)
5098

99+
def display_installation_plan(component: str, dependencies: Set[str], dry_run: bool = False) -> None:
100+
"""Display what will be installed in a tree format"""
101+
tree = Tree(
102+
f"[bold cyan]{component}[/bold cyan] "
103+
f"[dim]({'preview' if dry_run else 'will be installed'})[/dim]"
104+
)
105+
106+
if dependencies:
107+
deps_branch = tree.add("[bold yellow]Dependencies[/bold yellow]")
108+
for dep in sorted(dependencies):
109+
deps_branch.add(f"[green]{dep}[/green]")
110+
111+
console.print(tree)
51112

52113
@app.command()
53114
def add(
54-
component: str = typer.Argument(..., help="Name of the component to install"),
55-
branch: str = typer.Option(
56-
DEFAULT_BRANCH, "--branch", "-b", help="Branch, tag, or commit to install from"
57-
),
58-
repo_url: str = typer.Option(
59-
DEFAULT_REPO_URL, "--repo-url", "-r", help="Repository URL to use"
60-
),
61-
components_dir: Path = typer.Option(
62-
DEFAULT_COMPONENTS_DIR, "--components-dir", "-d", help="Directory to install components"
63-
)
115+
component: str = typer.Argument(..., help="Name of the component to install"),
116+
branch: str = typer.Option(
117+
DEFAULT_BRANCH, "--branch", "-b", help="Branch, tag, or commit to install from"
118+
),
119+
repo_url: str = typer.Option(
120+
DEFAULT_REPO_URL, "--repo-url", "-r", help="Repository URL to use"
121+
),
122+
components_dir: Path = typer.Option(
123+
DEFAULT_COMPONENTS_DIR, "--components-dir", "-d", help="Directory to install components"
124+
),
125+
with_deps: bool = typer.Option(
126+
True, "--with-deps/--no-deps", help="Install dependencies automatically"
127+
),
128+
dry_run: bool = typer.Option(
129+
False, "--dry-run", help="Preview what would be installed without making changes"
130+
)
64131
) -> None:
65132
"""Add a component to your project."""
66133
try:
67-
add_component(component, components_dir, repo_url, branch)
134+
# Load dependencies
135+
deps_map = load_dependencies()
136+
137+
# Normalize component name
138+
component = normalize_component_name(component)
139+
140+
# Get all dependencies if requested
141+
components_to_install = {component}
142+
if with_deps:
143+
dependencies = set(deps_map.get(component, []))
144+
if dependencies:
145+
console.print(f"\n[yellow]Debug: Found dependencies: {dependencies}[/yellow]")
146+
components_to_install.update(dependencies)
147+
else:
148+
dependencies = set()
149+
150+
# Display installation plan
151+
display_installation_plan(component, dependencies, dry_run)
152+
153+
if dry_run:
154+
console.print("\n[yellow]Dry run complete. No changes made.[/yellow]")
155+
return
156+
157+
# Install each component separately with its own exclude pattern
158+
installed = []
159+
for comp in sorted(components_to_install):
160+
console.print(f"\n[yellow]Debug: Installing component: {comp}[/yellow]")
161+
add_component(comp, components_dir, repo_url, branch, dry_run)
162+
installed.append(comp)
163+
164+
# Show completion message
165+
deps_msg = "\n[cyan]Installed dependencies:[/cyan]\n" + "\n".join(
166+
f" - {comp}" for comp in installed[1:]
167+
) if len(installed) > 1 else ""
68168

69169
console.print(
70170
Panel(
71-
f"[green]✓[/green] Added {component} component\n\n"
72-
f"[cyan] components-dir={components_dir}[/cyan]",
171+
f"[green]✓[/green] Added {component} component{deps_msg}\n\n"
172+
f"[cyan]components-dir={components_dir}[/cyan]",
73173
title="Installation Complete",
74174
border_style="green",
75175
)
76176
)
177+
77178
except Exception as e:
78179
console.print(f"[red]Error: {str(e)}[/red]")
79180
raise typer.Exit(1)
80181

81-
82182
@app.command()
83183
def init(
84-
components_dir: Path = COMPONENTS_DIR_OPTION,
184+
components_dir: Path = typer.Option(
185+
DEFAULT_COMPONENTS_DIR,
186+
"--components-dir",
187+
"-d",
188+
help="Directory to install components"
189+
),
85190
) -> None:
86191
"""Initialize project for basic-components."""
87-
# Create components directory structure
88192
components_dir.mkdir(parents=True, exist_ok=True)
89193

90194
console.print(
@@ -106,6 +210,5 @@ def init(
106210
)
107211
)
108212

109-
110213
if __name__ == "__main__":
111214
app()
File renamed without changes.
File renamed without changes.

pyproject.toml

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ authors = [
77
]
88
readme = "README.md"
99
requires-python = ">=3.10"
10-
license = { text = "MIT" }
1110
keywords = [
1211
"web",
1312
"components",
@@ -43,23 +42,20 @@ classifiers = [
4342
"Topic :: Software Development :: User Interfaces",
4443
"Typing :: Typed",
4544
]
46-
47-
# CLI tool dependencies
4845
dependencies = [
4946
"copier>=9.4.1",
5047
"typer>=0.9.0",
5148
"rich>=13.7.0",
5249
]
5350

54-
[project.optional-dependencies]
55-
51+
[project.license]
52+
text = "MIT"
5653

57-
# For using the utility functions in basic_components/utils
54+
[project.optional-dependencies]
5855
utils = [
5956
"jinjax>=0.47",
6057
"jinja2>=3.1.3",
6158
]
62-
6359
docs = [
6460
"fastapi[standard]>=0.115.4",
6561
"jinjax[whitenoise]>=0.47",
@@ -70,8 +66,8 @@ docs = [
7066
"uvicorn>=0.32.0",
7167
"wtforms>=3.2.1",
7268
"jinja2>=3.1.3",
73-
"python-frontmatter>=1.1.0", # For markdown metadata
74-
"pygments>=2.17.2", # For code highlighting
69+
"python-frontmatter>=1.1.0",
70+
"pygments>=2.17.2",
7571
"loguru>=0.7.2",
7672
"pydantic>=2.9.2",
7773
"pydantic-settings>=2.6.0",
@@ -85,18 +81,15 @@ docs = [
8581
"setuptools>=75.5.0",
8682
"copier>=9.4.1",
8783
"tomli>=2.0.2",
84+
"tomli-w>=1.1.0",
8885
]
89-
90-
# Development dependencies
9186
dev = [
9287
"black>=24.1.0",
9388
"isort>=5.13.0",
9489
"mypy>=1.8.0",
9590
"ruff>=0.2.0",
9691
"python-semantic-release>=9.14.0",
9792
]
98-
99-
# Full install with all features
10093
full = [
10194
"basic-components[utils]",
10295
"basic-components[docs]",
@@ -114,30 +107,34 @@ Changelog = "https://github.com/basicmachines-co/basic-components/blob/main/CHAN
114107
Issues = "https://github.com/basicmachines-co/basic-components/issues"
115108

116109
[build-system]
117-
requires = ["hatchling"]
110+
requires = [
111+
"hatchling",
112+
]
118113
build-backend = "hatchling.build"
119114

120-
[tool.hatch.build.targets.wheel]
121-
packages = ["basic_components"]
122-
123-
[tool.hatch.build.targets.wheel.scripts]
124-
components = "basic_components.cli:app"
125-
126115
[tool.hatch.build]
127116
include = [
128117
"basic_components/**/*.py",
118+
"basic_components/component_dependencies.toml",
129119
]
130120

131-
[tool.basic-components]
132-
components_dir = "components"
133-
style = "default"
121+
[tool.hatch.build.targets.wheel]
122+
packages = [
123+
"basic_components",
124+
]
125+
126+
[tool.hatch.build.targets.wheel.scripts]
127+
components = "basic_components.cli:app"
134128

135129
[tool.pytest.ini_options]
136-
testpaths = ["tests"]
137-
python_files = ["test_*.py"]
130+
testpaths = [
131+
"tests",
132+
]
133+
python_files = [
134+
"test_*.py",
135+
]
138136
addopts = "-ra -q"
139137

140-
141138
[tool.semantic_release]
142139
version_variable = "basic_components/__init__.py:__version__"
143140
version_toml = [
@@ -149,4 +146,9 @@ changelog_file = "CHANGELOG.md"
149146
build_command = "pip install uv && uv build"
150147
dist_path = "dist/"
151148
upload_to_pypi = true
152-
commit_message = "chore(release): {version} [skip ci]"
149+
commit_message = "chore(release): {version} [skip ci]"
150+
151+
152+
[tool.basic-components]
153+
components_dir = "components"
154+

0 commit comments

Comments
 (0)