Skip to content

Commit 0606f68

Browse files
fix(cli): derive uipath pin in new scaffold from installed version (#1869)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent fea41bb commit 0606f68

5 files changed

Lines changed: 127 additions & 6 deletions

File tree

‎packages/uipath/docs/core/agents.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ version = "0.1.0"
119119
description = "..."
120120
authors = [{ name = "Your Name", email = "you@example.com" }]
121121
requires-python = ">=3.11"
122-
dependencies = ["uipath>=2.0", "uipath-langchain>=2.0"]
122+
dependencies = ["uipath>=2.0", "uipath-langchain>=0.16"]
123123
```
124124

125125
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.

‎packages/uipath/pyproject.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.14.8"
3+
version = "2.14.9"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
@@ -81,6 +81,7 @@ dev = [
8181
"types-toml>=0.10.8",
8282
"types-PyYAML>=6.0",
8383
"pytest-timeout>=2.4.0",
84+
"packaging>=24.0",
8485
"uipath-ipc>=2.5.1,<2.6.0",
8586
]
8687

‎packages/uipath/src/uipath/_cli/cli_new.py‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,48 @@
11
import json
22
import os
3+
import re
34
import shutil
5+
import uuid
46

57
import click
68

79
from uipath.platform.constants import PYTHON_CONFIGURATION_FILE, UIPATH_CONFIG_FILE
810

911
from ._telemetry import track_command
1012
from ._utils._console import ConsoleLogger
13+
from ._utils._project_files import resolve_existing_project_id
1114
from .middlewares import Middlewares
1215

1316
console = ConsoleLogger()
1417

18+
FALLBACK_UIPATH_MINOR = "2.14"
19+
20+
21+
def _minor_range_spec(major: int, minor: int) -> str:
22+
return f"uipath>={major}.{minor}.0, <{major}.{minor + 1}.0"
23+
24+
25+
def _fallback_uipath_dependency_spec() -> str:
26+
major, minor = (int(part) for part in FALLBACK_UIPATH_MINOR.split("."))
27+
return _minor_range_spec(major, minor)
28+
29+
30+
def _uipath_dependency_spec() -> str:
31+
from . import _get_safe_version
32+
33+
installed = _get_safe_version()
34+
# Strip pre-release/dev/local suffixes: only the leading "major.minor" matters.
35+
match = re.match(r"^(\d+)\.(\d+)", installed)
36+
if match is None:
37+
fallback = _fallback_uipath_dependency_spec()
38+
console.warning(
39+
f"Could not determine the installed 'uipath' version ('{installed}'); "
40+
f"falling back to '{fallback}'. Pin it manually if needed."
41+
)
42+
return fallback
43+
44+
return _minor_range_spec(int(match.group(1)), int(match.group(2)))
45+
1546

1647
def generate_script(target_directory):
1748
template_path = os.path.join(
@@ -30,7 +61,7 @@ def generate_pyproject(target_directory, project_name):
3061
description = "{project_name}"
3162
authors = [{{ name = "John Doe", email = "john.doe@myemail.com" }}]
3263
dependencies = [
33-
"uipath>=2.10.0, <2.11.0"
64+
"{_uipath_dependency_spec()}"
3465
]
3566
requires-python = ">=3.11"
3667
"""
@@ -41,7 +72,8 @@ def generate_pyproject(target_directory, project_name):
4172

4273
def generate_uipath_json(target_directory):
4374
uipath_json_path = os.path.join(target_directory, UIPATH_CONFIG_FILE)
44-
uipath_config = {"functions": {"main": "main.py:main"}}
75+
project_id = resolve_existing_project_id(target_directory) or str(uuid.uuid4())
76+
uipath_config = {"id": project_id, "functions": {"main": "main.py:main"}}
4577

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

87119

‎packages/uipath/tests/cli/test_new.py‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import json
12
import os
3+
import re
4+
import uuid
5+
from importlib.metadata import version
26
from unittest.mock import patch
37

48
from click.testing import CliRunner
9+
from packaging.specifiers import SpecifierSet
510

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

25+
def test_new_project_writes_uipath_json_id(
26+
self, runner: CliRunner, temp_dir: str
27+
) -> None:
28+
"""uipath.json gets a GUID id up front so later commands don't warn."""
29+
with runner.isolated_filesystem(temp_dir=temp_dir):
30+
result = runner.invoke(cli, ["new", "my_project"])
31+
assert result.exit_code == 0
32+
with open("uipath.json") as f:
33+
config = json.load(f)
34+
uuid.UUID(config["id"])
35+
assert config["functions"] == {"main": "main.py:main"}
36+
2037
def test_new_project_without_name(self, runner: CliRunner, temp_dir: str) -> None:
2138
"""Test creating a new project without specifying a name."""
2239
with runner.isolated_filesystem(temp_dir=temp_dir):
@@ -79,3 +96,72 @@ def test_new_project_error_handling(self, runner: CliRunner, temp_dir: str) -> N
7996
result = runner.invoke(cli, ["new", "my_project"])
8097
assert result.exit_code == 1
8198
assert "Created 'main.py' file." not in result.output
99+
100+
101+
class TestUipathDependencySpec:
102+
"""The scaffolded pin must follow the installed uipath version."""
103+
104+
def _written_pin(self, temp_dir: str) -> str:
105+
from uipath._cli.cli_new import generate_pyproject
106+
107+
generate_pyproject(temp_dir, "demo")
108+
with open(os.path.join(temp_dir, "pyproject.toml")) as f:
109+
content = f.read()
110+
match = re.search(r'"(uipath[^"]*)"', content)
111+
assert match is not None, content
112+
return match.group(1)
113+
114+
def test_pin_derived_from_installed_version(self, temp_dir: str) -> None:
115+
with patch("uipath._cli._get_safe_version", return_value="2.14.7"):
116+
assert self._written_pin(temp_dir) == "uipath>=2.14.0, <2.15.0"
117+
118+
def test_pin_strips_prerelease_suffix(self, temp_dir: str) -> None:
119+
with patch("uipath._cli._get_safe_version", return_value="2.15.0rc1"):
120+
assert self._written_pin(temp_dir) == "uipath>=2.15.0, <2.16.0"
121+
122+
def test_pin_falls_back_when_version_unknown(self, temp_dir: str) -> None:
123+
from uipath._cli.cli_new import _fallback_uipath_dependency_spec, console
124+
125+
# _get_safe_version() returns "unknown" on PackageNotFoundError.
126+
with (
127+
patch("uipath._cli._get_safe_version", return_value="unknown"),
128+
patch.object(console, "warning") as mock_warning,
129+
):
130+
assert self._written_pin(temp_dir) == _fallback_uipath_dependency_spec()
131+
mock_warning.assert_called_once()
132+
assert (
133+
"Could not determine the installed 'uipath' version"
134+
in (mock_warning.call_args.args[0])
135+
)
136+
137+
def test_fallback_pin_admits_installed_uipath(self) -> None:
138+
"""Guard: a release PR that forgets to bump FALLBACK_UIPATH_MINOR fails CI."""
139+
from uipath._cli.cli_new import _fallback_uipath_dependency_spec
140+
141+
spec = _fallback_uipath_dependency_spec().removeprefix("uipath")
142+
installed = version("uipath")
143+
assert SpecifierSet(spec).contains(installed, prereleases=True), (
144+
f"fallback pin '{spec}' does not admit installed uipath {installed}; "
145+
"bump FALLBACK_UIPATH_MINOR in cli_new.py"
146+
)
147+
148+
def test_scaffolded_pin_contains_installed_uipath(
149+
self, runner: CliRunner, temp_dir: str
150+
) -> None:
151+
"""Regression guard: runs against the real installed package, not a mock.
152+
153+
A stale hard-coded range would make ``uv sync`` downgrade the project's
154+
venv right after ``uipath new``.
155+
"""
156+
with runner.isolated_filesystem(temp_dir=temp_dir):
157+
result = runner.invoke(cli, ["new", "demo"])
158+
assert result.exit_code == 0
159+
with open("pyproject.toml") as f:
160+
content = f.read()
161+
match = re.search(r'"uipath([^"]*)"', content)
162+
assert match is not None, content
163+
pin = SpecifierSet(match.group(1))
164+
installed = version("uipath")
165+
assert pin.contains(installed, prereleases=True), (
166+
f"scaffolded pin '{pin}' does not contain installed uipath {installed}"
167+
)

‎packages/uipath/uv.lock‎

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)