From d74ea0eb55b11a9b6b403541e24de80960921751 Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 17 Nov 2024 15:39:51 -0600 Subject: [PATCH] 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. --- basic_components/cli.py | 169 ++++++++++++++---- .../wtform}/WTForm.jinja | 0 .../ModeToggle.jinja | 0 pyproject.toml | 56 +++--- uv.lock | 33 ++-- 5 files changed, 186 insertions(+), 72 deletions(-) rename components/ui/{extended => integrations/wtform}/WTForm.jinja (100%) rename components/ui/{extended => mode_toggle}/ModeToggle.jinja (100%) diff --git a/basic_components/cli.py b/basic_components/cli.py index 4b892887..690e2118 100644 --- a/basic_components/cli.py +++ b/basic_components/cli.py @@ -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", @@ -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( @@ -106,6 +210,5 @@ def init( ) ) - if __name__ == "__main__": app() \ No newline at end of file diff --git a/components/ui/extended/WTForm.jinja b/components/ui/integrations/wtform/WTForm.jinja similarity index 100% rename from components/ui/extended/WTForm.jinja rename to components/ui/integrations/wtform/WTForm.jinja diff --git a/components/ui/extended/ModeToggle.jinja b/components/ui/mode_toggle/ModeToggle.jinja similarity index 100% rename from components/ui/extended/ModeToggle.jinja rename to components/ui/mode_toggle/ModeToggle.jinja diff --git a/pyproject.toml b/pyproject.toml index ff8fa058..743106f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ authors = [ ] readme = "README.md" requires-python = ">=3.10" -license = { text = "MIT" } keywords = [ "web", "components", @@ -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", @@ -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", @@ -85,9 +81,8 @@ 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", @@ -95,8 +90,6 @@ dev = [ "ruff>=0.2.0", "python-semantic-release>=9.14.0", ] - -# Full install with all features full = [ "basic-components[utils]", "basic-components[docs]", @@ -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 = [ @@ -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]" \ No newline at end of file +commit_message = "chore(release): {version} [skip ci]" + + +[tool.basic-components] +components_dir = "components" + diff --git a/uv.lock b/uv.lock index a5b8042f..2626b20b 100644 --- a/uv.lock +++ b/uv.lock @@ -56,19 +56,20 @@ wheels = [ [[package]] name = "basic-components" -version = "0.1.0" +version = "0.1.7" source = { editable = "." } - -[package.optional-dependencies] -cli = [ +dependencies = [ { name = "copier" }, { name = "rich" }, { name = "typer" }, ] + +[package.optional-dependencies] dev = [ { name = "black" }, { name = "isort" }, { name = "mypy" }, + { name = "python-semantic-release" }, { name = "ruff" }, ] docs = [ @@ -118,12 +119,11 @@ full = [ { name = "pytest" }, { name = "pytest-playwright" }, { name = "python-frontmatter" }, - { name = "rich" }, + { name = "python-semantic-release" }, { name = "ruff" }, { name = "setuptools" }, { name = "starlette-wtf" }, { name = "tomli" }, - { name = "typer" }, { name = "uvicorn" }, { name = "watchfiles" }, { name = "websockets" }, @@ -136,18 +136,17 @@ utils = [ [package.dependency-groups] dev = [ - { name = "python-semantic-release" }, + { name = "tomli-w" }, ] [package.metadata] requires-dist = [ { name = "arel", marker = "extra == 'docs'", specifier = ">=0.3.0" }, - { name = "basic-components", extras = ["cli"], marker = "extra == 'full'" }, { name = "basic-components", extras = ["dev"], marker = "extra == 'full'" }, { name = "basic-components", extras = ["docs"], marker = "extra == 'full'" }, { name = "basic-components", extras = ["utils"], marker = "extra == 'full'" }, { name = "black", marker = "extra == 'dev'", specifier = ">=24.1.0" }, - { name = "copier", marker = "extra == 'cli'", specifier = ">=9.4.1" }, + { name = "copier", specifier = ">=9.4.1" }, { name = "copier", marker = "extra == 'docs'", specifier = ">=9.4.1" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'docs'", specifier = ">=0.115.4" }, { name = "icecream", marker = "extra == 'docs'", specifier = ">=2.1.3" }, @@ -168,12 +167,13 @@ requires-dist = [ { name = "pytest", marker = "extra == 'docs'", specifier = ">=8.3.3" }, { name = "pytest-playwright", marker = "extra == 'docs'", specifier = ">=0.5.2" }, { name = "python-frontmatter", marker = "extra == 'docs'", specifier = ">=1.1.0" }, - { name = "rich", marker = "extra == 'cli'", specifier = ">=13.7.0" }, + { name = "python-semantic-release", marker = "extra == 'dev'", specifier = ">=9.14.0" }, + { name = "rich", specifier = ">=13.7.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.2.0" }, { name = "setuptools", marker = "extra == 'docs'", specifier = ">=75.5.0" }, { name = "starlette-wtf", marker = "extra == 'docs'", specifier = ">=0.4.5" }, { name = "tomli", marker = "extra == 'docs'", specifier = ">=2.0.2" }, - { name = "typer", marker = "extra == 'cli'", specifier = ">=0.9.0" }, + { name = "typer", specifier = ">=0.9.0" }, { name = "uvicorn", marker = "extra == 'docs'", specifier = ">=0.32.0" }, { name = "watchfiles", marker = "extra == 'docs'", specifier = ">=0.24.0" }, { name = "websockets", marker = "extra == 'docs'", specifier = ">=13.1" }, @@ -181,7 +181,7 @@ requires-dist = [ ] [package.metadata.dependency-groups] -dev = [{ name = "python-semantic-release", specifier = ">=9.14.0" }] +dev = [{ name = "tomli-w", specifier = ">=1.1.0" }] [[package]] name = "black" @@ -1438,6 +1438,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, ] +[[package]] +name = "tomli-w" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, +] + [[package]] name = "tomlkit" version = "0.13.2"