Skip to content

Commit

Permalink
refactor: restructure component files and update dependencies
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
phernandez committed Nov 17, 2024
1 parent b6e4fc8 commit d74ea0e
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 72 deletions.
169 changes: 136 additions & 33 deletions basic_components/cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from pathlib import Path
from typing import List, Dict, Set
import tomli
import importlib.resources

import copier
import typer
from rich.console import Console
from rich.panel import Panel
from rich.tree import Tree

app = typer.Typer(
name="components",
Expand All @@ -18,73 +22,173 @@
DEFAULT_COMPONENTS_DIR = Path("components/ui")
DEFAULT_BRANCH = "main"

# typer cli arg options
COMPONENTS_DIR_OPTION = typer.Option(DEFAULT_COMPONENTS_DIR, "--components-dir", "-d", help="Directory to update components in")
REPO_URL_OPTION = typer.Option(DEFAULT_REPO_URL, "--repo-url", "-r", help="Repository URL to update from")
BRANCH_OPTION = typer.Option(DEFAULT_BRANCH, "--branch", "-b", help="Branch, tag, or commit to update from")

def load_dependencies() -> Dict[str, List[str]]:
"""Load component dependencies from component_dependencies.toml within the package."""
try:
with importlib.resources.open_text('basic_components', 'component_dependencies.toml') as f:
toml_data = tomli.loads(f.read())
return toml_data.get('dependencies', {})
except Exception as e:
console.print(f"[red]Error loading dependencies: {e}[/red]")
return {}

def normalize_component_name(name: str) -> str:
"""Convert component references to normalized form"""
# Only normalize if it's an icon reference
if name.startswith("icons/"):
# If already in icons/Name format, return as-is
return name

if "/" not in name and name != "icons":
# Handle bare icon names (without icons/ prefix)
# Convert to PascalCase if needed
if name.lower() in {"check", "x", "moon", "sun"}:
return f"icons/{name.title()}"
# Handle compound names
if name.lower() in {"chevron-right", "chevron-down", "chevron-up", "chevrons-up-down"}:
parts = name.split("-")
pascal_name = "".join(p.title() for p in parts)
return f"icons/{pascal_name}"

return name

def get_component_pattern(component: str) -> str:
"""Get the file pattern for a component."""
if component.startswith("icons/"):
icon_name = component.split("/")[1]
return f"icons/{icon_name}Icon.jinja"
else:
return f"{component}/**"

def add_component(
component: str,
dest_dir: Path,
repo_url: str = DEFAULT_REPO_URL,
branch: str = DEFAULT_BRANCH,
component: str,
dest_dir: Path,
repo_url: str = DEFAULT_REPO_URL,
branch: str = DEFAULT_BRANCH,
dry_run: bool = False,
) -> None:
"""Add a specific component to the project."""
try:
console.print(f"[green]Installing {component} from '{repo_url}' ...[/green]")
console.print(f"[green]Installing {component}...[/green]")

# Get the pattern for this component
pattern = get_component_pattern(component)

# Build exclude list - exclude everything except our pattern
excludes = ["*", f"!{pattern}"]

# Debug output
console.print("[yellow]Debug: Copying with args:[/yellow]")
console.print(f" src_path: {repo_url}")
console.print(f" dst_path: {dest_dir}")
console.print(f" exclude patterns: {excludes}")
console.print(f" vcs_ref: {branch}")

copier.run_copy(
src_path=repo_url,
dst_path=str(dest_dir),
exclude=[
"*",
f"!{component}",
],
exclude=excludes,
vcs_ref=branch,
pretend=dry_run,
)

except Exception as e: # pyright: ignore [reportAttributeAccessIssue]
console.print(f"[red]Error: {str(e)}[/red]")
except Exception as e:
console.print(f"[red]Error installing {component}: {str(e)}[/red]")
raise typer.Exit(1)

def display_installation_plan(component: str, dependencies: Set[str], dry_run: bool = False) -> None:
"""Display what will be installed in a tree format"""
tree = Tree(
f"[bold cyan]{component}[/bold cyan] "
f"[dim]({'preview' if dry_run else 'will be installed'})[/dim]"
)

if dependencies:
deps_branch = tree.add("[bold yellow]Dependencies[/bold yellow]")
for dep in sorted(dependencies):
deps_branch.add(f"[green]{dep}[/green]")

console.print(tree)

@app.command()
def add(
component: str = typer.Argument(..., help="Name of the component to install"),
branch: str = typer.Option(
DEFAULT_BRANCH, "--branch", "-b", help="Branch, tag, or commit to install from"
),
repo_url: str = typer.Option(
DEFAULT_REPO_URL, "--repo-url", "-r", help="Repository URL to use"
),
components_dir: Path = typer.Option(
DEFAULT_COMPONENTS_DIR, "--components-dir", "-d", help="Directory to install components"
)
component: str = typer.Argument(..., help="Name of the component to install"),
branch: str = typer.Option(
DEFAULT_BRANCH, "--branch", "-b", help="Branch, tag, or commit to install from"
),
repo_url: str = typer.Option(
DEFAULT_REPO_URL, "--repo-url", "-r", help="Repository URL to use"
),
components_dir: Path = typer.Option(
DEFAULT_COMPONENTS_DIR, "--components-dir", "-d", help="Directory to install components"
),
with_deps: bool = typer.Option(
True, "--with-deps/--no-deps", help="Install dependencies automatically"
),
dry_run: bool = typer.Option(
False, "--dry-run", help="Preview what would be installed without making changes"
)
) -> None:
"""Add a component to your project."""
try:
add_component(component, components_dir, repo_url, branch)
# Load dependencies
deps_map = load_dependencies()

# Normalize component name
component = normalize_component_name(component)

# Get all dependencies if requested
components_to_install = {component}
if with_deps:
dependencies = set(deps_map.get(component, []))
if dependencies:
console.print(f"\n[yellow]Debug: Found dependencies: {dependencies}[/yellow]")
components_to_install.update(dependencies)
else:
dependencies = set()

# Display installation plan
display_installation_plan(component, dependencies, dry_run)

if dry_run:
console.print("\n[yellow]Dry run complete. No changes made.[/yellow]")
return

# Install each component separately with its own exclude pattern
installed = []
for comp in sorted(components_to_install):
console.print(f"\n[yellow]Debug: Installing component: {comp}[/yellow]")
add_component(comp, components_dir, repo_url, branch, dry_run)
installed.append(comp)

# Show completion message
deps_msg = "\n[cyan]Installed dependencies:[/cyan]\n" + "\n".join(
f" - {comp}" for comp in installed[1:]
) if len(installed) > 1 else ""

console.print(
Panel(
f"[green]✓[/green] Added {component} component\n\n"
f"[cyan] components-dir={components_dir}[/cyan]",
f"[green]✓[/green] Added {component} component{deps_msg}\n\n"
f"[cyan]components-dir={components_dir}[/cyan]",
title="Installation Complete",
border_style="green",
)
)

except Exception as e:
console.print(f"[red]Error: {str(e)}[/red]")
raise typer.Exit(1)


@app.command()
def init(
components_dir: Path = COMPONENTS_DIR_OPTION,
components_dir: Path = typer.Option(
DEFAULT_COMPONENTS_DIR,
"--components-dir",
"-d",
help="Directory to install components"
),
) -> None:
"""Initialize project for basic-components."""
# Create components directory structure
components_dir.mkdir(parents=True, exist_ok=True)

console.print(
Expand All @@ -106,6 +210,5 @@ def init(
)
)


if __name__ == "__main__":
app()
File renamed without changes.
File renamed without changes.
56 changes: 29 additions & 27 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ authors = [
]
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
keywords = [
"web",
"components",
Expand Down Expand Up @@ -43,23 +42,20 @@ classifiers = [
"Topic :: Software Development :: User Interfaces",
"Typing :: Typed",
]

# CLI tool dependencies
dependencies = [
"copier>=9.4.1",
"typer>=0.9.0",
"rich>=13.7.0",
]

[project.optional-dependencies]

[project.license]
text = "MIT"

# For using the utility functions in basic_components/utils
[project.optional-dependencies]
utils = [
"jinjax>=0.47",
"jinja2>=3.1.3",
]

docs = [
"fastapi[standard]>=0.115.4",
"jinjax[whitenoise]>=0.47",
Expand All @@ -70,8 +66,8 @@ docs = [
"uvicorn>=0.32.0",
"wtforms>=3.2.1",
"jinja2>=3.1.3",
"python-frontmatter>=1.1.0", # For markdown metadata
"pygments>=2.17.2", # For code highlighting
"python-frontmatter>=1.1.0",
"pygments>=2.17.2",
"loguru>=0.7.2",
"pydantic>=2.9.2",
"pydantic-settings>=2.6.0",
Expand All @@ -85,18 +81,15 @@ docs = [
"setuptools>=75.5.0",
"copier>=9.4.1",
"tomli>=2.0.2",
"tomli-w>=1.1.0",
]

# Development dependencies
dev = [
"black>=24.1.0",
"isort>=5.13.0",
"mypy>=1.8.0",
"ruff>=0.2.0",
"python-semantic-release>=9.14.0",
]

# Full install with all features
full = [
"basic-components[utils]",
"basic-components[docs]",
Expand All @@ -114,30 +107,34 @@ Changelog = "https://github.com/basicmachines-co/basic-components/blob/main/CHAN
Issues = "https://github.com/basicmachines-co/basic-components/issues"

[build-system]
requires = ["hatchling"]
requires = [
"hatchling",
]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["basic_components"]

[tool.hatch.build.targets.wheel.scripts]
components = "basic_components.cli:app"

[tool.hatch.build]
include = [
"basic_components/**/*.py",
"basic_components/component_dependencies.toml",
]

[tool.basic-components]
components_dir = "components"
style = "default"
[tool.hatch.build.targets.wheel]
packages = [
"basic_components",
]

[tool.hatch.build.targets.wheel.scripts]
components = "basic_components.cli:app"

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
testpaths = [
"tests",
]
python_files = [
"test_*.py",
]
addopts = "-ra -q"


[tool.semantic_release]
version_variable = "basic_components/__init__.py:__version__"
version_toml = [
Expand All @@ -149,4 +146,9 @@ changelog_file = "CHANGELOG.md"
build_command = "pip install uv && uv build"
dist_path = "dist/"
upload_to_pypi = true
commit_message = "chore(release): {version} [skip ci]"
commit_message = "chore(release): {version} [skip ci]"


[tool.basic-components]
components_dir = "components"

Loading

0 comments on commit d74ea0e

Please sign in to comment.