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/docs/core/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ version = "0.1.0"
description = "..."
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.11"
dependencies = ["uipath>=2.0", "uipath-langchain>=2.0"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a documentation inconsistency that was referencing an invalid uipath-langchain version.

dependencies = ["uipath>=2.0", "uipath-langchain>=0.16"]
```

Standard metadata plus the framework dependency (`uipath-langchain` here). The framework graph file and this dependency identify the project as a coded agent — `pyproject.toml` needs no UiPath-specific entries, and `uipath.json` carries no agent entry.
Expand Down
3 changes: 2 additions & 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.8"
version = "2.14.9"
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 Expand Up @@ -81,6 +81,7 @@ dev = [
"types-toml>=0.10.8",
"types-PyYAML>=6.0",
"pytest-timeout>=2.4.0",
"packaging>=24.0",
"uipath-ipc>=2.5.1,<2.6.0",
]

Expand Down
38 changes: 35 additions & 3 deletions packages/uipath/src/uipath/_cli/cli_new.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
import json
import os
import re
import shutil
import uuid

import click

from uipath.platform.constants import PYTHON_CONFIGURATION_FILE, UIPATH_CONFIG_FILE

from ._telemetry import track_command
from ._utils._console import ConsoleLogger
from ._utils._project_files import resolve_existing_project_id
from .middlewares import Middlewares

console = ConsoleLogger()

FALLBACK_UIPATH_MINOR = "2.14"


def _minor_range_spec(major: int, minor: int) -> str:
return f"uipath>={major}.{minor}.0, <{major}.{minor + 1}.0"


def _fallback_uipath_dependency_spec() -> str:
major, minor = (int(part) for part in FALLBACK_UIPATH_MINOR.split("."))
return _minor_range_spec(major, minor)


def _uipath_dependency_spec() -> str:
from . import _get_safe_version

installed = _get_safe_version()
# Strip pre-release/dev/local suffixes: only the leading "major.minor" matters.
match = re.match(r"^(\d+)\.(\d+)", installed)
if match is None:
fallback = _fallback_uipath_dependency_spec()
console.warning(
f"Could not determine the installed 'uipath' version ('{installed}'); "
f"falling back to '{fallback}'. Pin it manually if needed."
)
return fallback

return _minor_range_spec(int(match.group(1)), int(match.group(2)))


def generate_script(target_directory):
template_path = os.path.join(
Expand All @@ -30,7 +61,7 @@ def generate_pyproject(target_directory, project_name):
description = "{project_name}"
authors = [{{ name = "John Doe", email = "john.doe@myemail.com" }}]
dependencies = [
"uipath>=2.10.0, <2.11.0"
"{_uipath_dependency_spec()}"
]
requires-python = ">=3.11"
"""
Expand All @@ -41,7 +72,8 @@ def generate_pyproject(target_directory, project_name):

def generate_uipath_json(target_directory):
uipath_json_path = os.path.join(target_directory, UIPATH_CONFIG_FILE)
uipath_config = {"functions": {"main": "main.py:main"}}
project_id = resolve_existing_project_id(target_directory) or str(uuid.uuid4())
uipath_config = {"id": project_id, "functions": {"main": "main.py:main"}}

with open(uipath_json_path, "w") as f:
json.dump(uipath_config, f, indent=2)
Expand Down Expand Up @@ -81,7 +113,7 @@ def new(name: str):
console.success(f"Created '{UIPATH_CONFIG_FILE}' file.")
init_command = """uipath init"""
run_command = """uipath run main '{"message": "Hello World!"}'"""
console.hint(f""" Initialize project: {click.style(init_command, fg="cyan")}""")
console.hint(f"""Initialize project: {click.style(init_command, fg="cyan")}""")
console.hint(f"""Run project: {click.style(run_command, fg="cyan")}""")


Expand Down
86 changes: 86 additions & 0 deletions packages/uipath/tests/cli/test_new.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import json
import os
import re
import uuid
from importlib.metadata import version
from unittest.mock import patch

from click.testing import CliRunner
from packaging.specifiers import SpecifierSet

from uipath._cli import cli
from uipath._cli.middlewares import MiddlewareResult
Expand All @@ -17,6 +22,18 @@ def test_new_project_creation(self, runner: CliRunner, temp_dir: str) -> None:
assert os.path.exists("main.py")
assert os.path.exists("pyproject.toml")

def test_new_project_writes_uipath_json_id(
self, runner: CliRunner, temp_dir: str
) -> None:
"""uipath.json gets a GUID id up front so later commands don't warn."""
with runner.isolated_filesystem(temp_dir=temp_dir):
result = runner.invoke(cli, ["new", "my_project"])
assert result.exit_code == 0
with open("uipath.json") as f:
config = json.load(f)
uuid.UUID(config["id"])
assert config["functions"] == {"main": "main.py:main"}

def test_new_project_without_name(self, runner: CliRunner, temp_dir: str) -> None:
"""Test creating a new project without specifying a name."""
with runner.isolated_filesystem(temp_dir=temp_dir):
Expand Down Expand Up @@ -79,3 +96,72 @@ def test_new_project_error_handling(self, runner: CliRunner, temp_dir: str) -> N
result = runner.invoke(cli, ["new", "my_project"])
assert result.exit_code == 1
assert "Created 'main.py' file." not in result.output


class TestUipathDependencySpec:
"""The scaffolded pin must follow the installed uipath version."""

def _written_pin(self, temp_dir: str) -> str:
from uipath._cli.cli_new import generate_pyproject

generate_pyproject(temp_dir, "demo")
with open(os.path.join(temp_dir, "pyproject.toml")) as f:
content = f.read()
match = re.search(r'"(uipath[^"]*)"', content)
assert match is not None, content
return match.group(1)

def test_pin_derived_from_installed_version(self, temp_dir: str) -> None:
with patch("uipath._cli._get_safe_version", return_value="2.14.7"):
assert self._written_pin(temp_dir) == "uipath>=2.14.0, <2.15.0"

def test_pin_strips_prerelease_suffix(self, temp_dir: str) -> None:
with patch("uipath._cli._get_safe_version", return_value="2.15.0rc1"):
assert self._written_pin(temp_dir) == "uipath>=2.15.0, <2.16.0"

def test_pin_falls_back_when_version_unknown(self, temp_dir: str) -> None:
from uipath._cli.cli_new import _fallback_uipath_dependency_spec, console

# _get_safe_version() returns "unknown" on PackageNotFoundError.
with (
patch("uipath._cli._get_safe_version", return_value="unknown"),
patch.object(console, "warning") as mock_warning,
):
assert self._written_pin(temp_dir) == _fallback_uipath_dependency_spec()
mock_warning.assert_called_once()
assert (
"Could not determine the installed 'uipath' version"
in (mock_warning.call_args.args[0])
)

def test_fallback_pin_admits_installed_uipath(self) -> None:
"""Guard: a release PR that forgets to bump FALLBACK_UIPATH_MINOR fails CI."""
from uipath._cli.cli_new import _fallback_uipath_dependency_spec

spec = _fallback_uipath_dependency_spec().removeprefix("uipath")
installed = version("uipath")
assert SpecifierSet(spec).contains(installed, prereleases=True), (
f"fallback pin '{spec}' does not admit installed uipath {installed}; "
"bump FALLBACK_UIPATH_MINOR in cli_new.py"
)

def test_scaffolded_pin_contains_installed_uipath(
self, runner: CliRunner, temp_dir: str
) -> None:
"""Regression guard: runs against the real installed package, not a mock.

A stale hard-coded range would make ``uv sync`` downgrade the project's
venv right after ``uipath new``.
"""
with runner.isolated_filesystem(temp_dir=temp_dir):
result = runner.invoke(cli, ["new", "demo"])
assert result.exit_code == 0
with open("pyproject.toml") as f:
content = f.read()
match = re.search(r'"uipath([^"]*)"', content)
assert match is not None, content
pin = SpecifierSet(match.group(1))
installed = version("uipath")
assert pin.contains(installed, prereleases=True), (
f"scaffolded pin '{pin}' does not contain installed uipath {installed}"
)
4 changes: 3 additions & 1 deletion packages/uipath/uv.lock

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

Loading