Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.14.28"
version = "2.14.29"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
35 changes: 35 additions & 0 deletions packages/uipath/src/uipath/_cli/cli_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import logging
import os
import re
import shutil
import uuid
from pathlib import Path
Expand All @@ -28,6 +29,7 @@
ENTRY_POINTS_FILE,
PYTHON_CONFIGURATION_FILE,
UIPATH_CONFIG_FILE,
UIPROJ_FILE,
)
from uipath.runtime import (
UiPathRuntimeContext,
Expand Down Expand Up @@ -207,6 +209,38 @@ def write_entry_points_file(entry_points: list[UiPathRuntimeSchema]) -> Path:
return entry_points_file_path


def _to_project_name(raw_name: str) -> str:
"""Normalize a display name into a valid ``pyproject.toml`` project name."""
normalized = re.sub(r"[^a-z0-9._-]+", "-", raw_name.strip().lower())
return normalized.strip("-._")


def ensure_project_is_scaffolded(current_directory: str) -> None:
"""Abort when the directory is empty or still an unscaffolded Studio Web template."""
visible_entries = {
entry for entry in os.listdir(current_directory) if not entry.startswith(".")
}

project_name = ""
if not visible_entries:
reason = "this directory is empty."
elif visible_entries == {UIPROJ_FILE}:
reason = f"'{UIPROJ_FILE}' is the only file in this directory."
try:
with open(os.path.join(current_directory, UIPROJ_FILE), "r") as f:
project_name = _to_project_name(str(json.load(f).get("Name") or ""))
except (OSError, json.JSONDecodeError):
pass
else:
return

new_command = f"uipath new {project_name or '<name>'}"
console.error(
f"This project has not been scaffolded yet: {reason}\n"
f"Scaffold it first by running:\n`{new_command}`"
)


def write_uiproj_file(
entry_point_schemas: list[UiPathRuntimeSchema],
current_directory: str,
Expand Down Expand Up @@ -424,6 +458,7 @@ def init(no_agents_md_override: bool) -> None:
"""Initialize the project."""
with console.spinner("Initializing UiPath project ..."):
current_directory = os.getcwd()
ensure_project_is_scaffolded(current_directory)
generate_env_file(current_directory)

async def initialize() -> list[UiPathRuntimeSchema]:
Expand Down
93 changes: 93 additions & 0 deletions packages/uipath/tests/cli/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,99 @@ def test_init_creates_uiproj_with_function_type(
assert uiproj["Description"] == "Test"
assert uiproj["MainFile"] is None

def test_init_aborts_on_unscaffolded_studio_web_project(
self, runner: CliRunner, temp_dir: str
) -> None:
"""Test that init stops before writing anything when only project.uiproj exists."""
with runner.isolated_filesystem(temp_dir=temp_dir):
with open("project.uiproj", "w") as f:
json.dump(
{
"Name": "My Function",
"ProjectType": "Function",
"MainFile": None,
},
f,
)

result = runner.invoke(cli, ["init"], env={})

assert result.exit_code == 1
assert "has not been scaffolded yet" in result.output
assert "uipath new my-function" in result.output
assert sorted(os.listdir(".")) == ["project.uiproj"]

def test_init_aborts_on_unscaffolded_project_without_name(
self, runner: CliRunner, temp_dir: str
) -> None:
"""Test that init falls back to a placeholder name when uiproj has none."""
with runner.isolated_filesystem(temp_dir=temp_dir):
with open("project.uiproj", "w") as f:
json.dump({"ProjectType": "Function"}, f)

result = runner.invoke(cli, ["init"], env={})

assert result.exit_code == 1
assert "uipath new <name>" in result.output
assert sorted(os.listdir(".")) == ["project.uiproj"]

def test_init_aborts_on_empty_directory(
self, runner: CliRunner, temp_dir: str
) -> None:
"""Test that init stops before writing anything in an empty directory."""
with runner.isolated_filesystem(temp_dir=temp_dir):
result = runner.invoke(cli, ["init"], env={})

assert result.exit_code == 1
assert "this directory is empty" in result.output
assert "uipath new <name>" in result.output
assert os.listdir(".") == []

def test_init_aborts_on_unscaffolded_project_with_malformed_uiproj(
self, runner: CliRunner, temp_dir: str
) -> None:
"""Test that an unreadable project.uiproj still yields the placeholder hint."""
with runner.isolated_filesystem(temp_dir=temp_dir):
with open("project.uiproj", "w") as f:
f.write("{ not json")

result = runner.invoke(cli, ["init"], env={})

assert result.exit_code == 1
assert "uipath new <name>" in result.output
assert sorted(os.listdir(".")) == ["project.uiproj"]

def test_init_ignores_hidden_entries_when_detecting_unscaffolded_project(
self, runner: CliRunner, temp_dir: str
) -> None:
"""Test that hidden files do not hide the unscaffolded Studio Web state."""
with runner.isolated_filesystem(temp_dir=temp_dir):
with open("project.uiproj", "w") as f:
json.dump({"Name": "Function", "ProjectType": "Function"}, f)
os.makedirs(".uipath")
with open(".env", "w") as f:
f.write("")

result = runner.invoke(cli, ["init"], env={})

assert result.exit_code == 1
assert "uipath new function" in result.output

def test_init_proceeds_when_uiproj_has_siblings(
self, runner: CliRunner, temp_dir: str
) -> None:
"""Test that an existing project.uiproj alongside sources does not trigger the guard."""
with runner.isolated_filesystem(temp_dir=temp_dir):
with open("project.uiproj", "w") as f:
json.dump({"Name": "my-project", "ProjectType": "Function"}, f)
self._generate_pyproject("my-project")

result = runner.invoke(cli, ["init"], env={})

assert result.exit_code == 0
assert "has not been scaffolded yet" not in result.output
assert "Updated 'project.uiproj' file" in result.output

def test_init_creates_uiproj_with_agent_type(
self, runner: CliRunner, temp_dir: str
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading