From c55dfcecf33c029f767cffd2a2b0d125c6dc9978 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 17 Dec 2024 16:16:49 -0800 Subject: [PATCH] Implement packaging update and remove, move project dependency installs to packaging.install_project and provide a gernalizable install command. --- agentstack/cli/init.py | 12 +-- agentstack/packaging.py | 82 ++++++++++++++----- .../pyproject.toml | 2 +- 3 files changed, 70 insertions(+), 26 deletions(-) diff --git a/agentstack/cli/init.py b/agentstack/cli/init.py index 6ce201a..6fd546d 100644 --- a/agentstack/cli/init.py +++ b/agentstack/cli/init.py @@ -37,26 +37,28 @@ def init_project( - copy project skeleton - install dependencies """ - welcome_message() + require_uv() - # conf.PATH may have been set by the argument parser, but if not, use the slug_name + # TODO prevent the user from passing the --path arguent to init if slug_name: conf.set_path(conf.PATH / slug_name) else: print("Error: No project directory specified.") - print("Run `agentstack init or use the --path flag.") + print("Run `agentstack init `") sys.exit(1) - if os.path.exists(conf.PATH): + if os.path.exists(conf.PATH): # cookiecutter requires the directory to not exist print(f"Error: Directory already exists: {conf.PATH}") sys.exit(1) + welcome_message() print(term_color("🦾 Creating a new AgentStack project...", 'blue')) print(f"Using project directory: {conf.PATH.absolute()}") + # copy the project skeleton, create a virtual environment, and install dependencies init_project_builder(slug_name, template, use_wizard) packaging.create_venv() - packaging.install('.') + packaging.install_project() print( "\n" diff --git a/agentstack/packaging.py b/agentstack/packaging.py index 729a28c..b472a51 100644 --- a/agentstack/packaging.py +++ b/agentstack/packaging.py @@ -5,47 +5,89 @@ import subprocess import select from agentstack import conf -from agentstack.exceptions import EnvironmentError DEFAULT_PYTHON_VERSION = "3.12" VENV_DIR_NAME: Path = Path(".venv") +# filter uv output by these words to only show useful progress messages +RE_UV_PROGRESS = re.compile(r'^(Resolved|Prepared|Installed|Uninstalled|Audited)') + + +# When calling `uv` we explicitly specify the --python executable to use so that +# the packages are installed into the correct virtual environment. +# In testing, when this was not set, packages could end up in the pyenv's +# site-packages directory; it's possible an environemnt variable can control this. + def install(package: str): - """ - Install a package with `uv`. - Filter output to only show useful progress messages. - """ - RE_USEFUL_PROGRESS = re.compile(r'^(Resolved|Prepared|Installed|Audited)') + """Install a package with `uv` and add it to pyproject.toml.""" def on_progress(line: str): - # only print these four messages: - # Resolved 78 packages in 225ms - # Prepared 12 packages in 915ms - # Installed 78 packages in 65ms - # Audited 1 package in 28ms - if RE_USEFUL_PROGRESS.match(line): + if RE_UV_PROGRESS.match(line): print(line.strip()) def on_error(line: str): print(f"uv: [error]\n {line.strip()}") - # explicitly specify the --python executable to use so that the packages - # are installed into the correct virtual environment _wrap_command_with_callbacks( - [get_uv_bin(), 'pip', 'install', '--python', '.venv/bin/python', package], + [get_uv_bin(), 'add', '--python', '.venv/bin/python', package], + on_progress=on_progress, + on_error=on_error, + ) + + +def install_project(): + """Install all dependencies for the user's project.""" + + def on_progress(line: str): + if RE_UV_PROGRESS.match(line): + print(line.strip()) + + def on_error(line: str): + print(f"uv: [error]\n {line.strip()}") + + _wrap_command_with_callbacks( + [get_uv_bin(), 'pip', 'install', '--python', '.venv/bin/python', '.'], on_progress=on_progress, on_error=on_error, ) def remove(package: str): - raise NotImplementedError("TODO `packaging.remove`") + """Uninstall a package with `uv`.""" + + # TODO it may be worth considering removing unused sub-dependencies as well + def on_progress(line: str): + if RE_UV_PROGRESS.match(line): + print(line.strip()) + + def on_error(line: str): + print(f"uv: [error]\n {line.strip()}") + + _wrap_command_with_callbacks( + [get_uv_bin(), 'remove', '--python', '.venv/bin/python', package], + on_progress=on_progress, + on_error=on_error, + ) def upgrade(package: str): - raise NotImplementedError("TODO `packaging.upgrade`") + """Upgrade a package with `uv`.""" + + # TODO should we try to update the project's pyproject.toml as well? + def on_progress(line: str): + if RE_UV_PROGRESS.match(line): + print(line.strip()) + + def on_error(line: str): + print(f"uv: [error]\n {line.strip()}") + + _wrap_command_with_callbacks( + [get_uv_bin(), 'pip', 'install', '-U', '--python', '.venv/bin/python', package], + on_progress=on_progress, + on_error=on_error, + ) def create_venv(python_version: str = DEFAULT_PYTHON_VERSION): @@ -53,10 +95,10 @@ def create_venv(python_version: str = DEFAULT_PYTHON_VERSION): if os.path.exists(conf.PATH / VENV_DIR_NAME): return # venv already exists - RE_USEFUL_PROGRESS = re.compile(r'^(Using|Creating)') + RE_VENV_PROGRESS = re.compile(r'^(Using|Creating)') def on_progress(line: str): - if RE_USEFUL_PROGRESS.match(line): + if RE_VENV_PROGRESS.match(line): print(line.strip()) def on_error(line: str): @@ -82,7 +124,7 @@ def get_uv_bin() -> str: def _setup_env() -> dict[str, str]: """Copy the current environment and add the virtual environment path for use by a subprocess.""" env = os.environ.copy() - env.setdefault("VIRTUAL_ENV", VENV_DIR_NAME) + env["VIRTUAL_ENV"] = str(conf.PATH / VENV_DIR_NAME.absolute()) env["UV_INTERNAL__PARENT_INTERPRETER"] = sys.executable return env diff --git a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/pyproject.toml b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/pyproject.toml index f931a97..f6dc4cf 100644 --- a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/pyproject.toml +++ b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/pyproject.toml @@ -9,5 +9,5 @@ license = { text = "{{cookiecutter.project_metadata.license}}" } requires-python = ">=3.10" dependencies = [ - "agentstack[{{cookiecutter.framework}}]=={{cookiecutter.project_metadata.agentstack_version}}", + "agentstack[{{cookiecutter.framework}}]>={{cookiecutter.project_metadata.agentstack_version}}", ] \ No newline at end of file