Skip to content

Commit 7035c31

Browse files
authored
Merge pull request #427 from UiPath/feat/dependencies
2 parents fa05aed + 78674df commit 7035c31

4 files changed

Lines changed: 1498 additions & 1475 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.0.75"
3+
version = "2.0.76"
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.10"

src/uipath/_cli/cli_pack.py

Lines changed: 166 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# type: ignore
22
import json
33
import os
4+
import re
45
import subprocess
56
import uuid
67
import zipfile
78
from string import Template
9+
from typing import Dict, Tuple
810

911
import click
1012

@@ -50,6 +52,7 @@ def check_config(directory):
5052
"entryPoints": config_data["entryPoints"],
5153
"version": toml_data["version"],
5254
"authors": toml_data["authors"],
55+
"dependencies": toml_data.get("dependencies", {}),
5356
}
5457

5558

@@ -113,7 +116,7 @@ def handle_uv_operations(directory):
113116
run_uv_lock(directory)
114117

115118

116-
def generate_operate_file(entryPoints):
119+
def generate_operate_file(entryPoints, dependencies=None):
117120
project_id = str(uuid.uuid4())
118121

119122
first_entry = entryPoints[0]
@@ -130,6 +133,10 @@ def generate_operate_file(entryPoints):
130133
"runtimeOptions": {"requiresUserInteraction": False, "isAttended": False},
131134
}
132135

136+
# Add dependencies if provided
137+
if dependencies:
138+
operate_json_data["dependencies"] = dependencies
139+
133140
return operate_json_data
134141

135142

@@ -149,40 +156,6 @@ def generate_bindings_content():
149156
return bindings_content
150157

151158

152-
def get_proposed_version(directory):
153-
output_dir = os.path.join(directory, ".uipath")
154-
if not os.path.exists(output_dir):
155-
return None
156-
157-
# Get all .nupkg files
158-
nupkg_files = [f for f in os.listdir(output_dir) if f.endswith(".nupkg")]
159-
if not nupkg_files:
160-
return None
161-
162-
# Sort by modification time to get most recent
163-
latest_file = max(
164-
nupkg_files, key=lambda f: os.path.getmtime(os.path.join(output_dir, f))
165-
)
166-
167-
# Extract version from filename
168-
# Remove .nupkg extension first
169-
name_version = latest_file[:-6]
170-
# Find 3rd last occurrence of . by splitting and joining parts
171-
parts = name_version.split(".")
172-
if len(parts) >= 3:
173-
version = ".".join(parts[-3:])
174-
else:
175-
version = name_version
176-
177-
# Increment patch version by 1
178-
try:
179-
major, minor, patch = version.split(".")
180-
new_version = f"{major}.{minor}.{int(patch) + 1}"
181-
return new_version
182-
except Exception:
183-
return "0.0.1"
184-
185-
186159
def generate_content_types_content():
187160
templates_path = os.path.join(
188161
os.path.dirname(__file__), "_templates", "[Content_Types].xml.template"
@@ -278,9 +251,10 @@ def pack_fn(
278251
version,
279252
authors,
280253
directory,
254+
dependencies=None,
281255
include_uv_lock=True,
282256
):
283-
operate_file = generate_operate_file(entryPoints)
257+
operate_file = generate_operate_file(entryPoints, dependencies)
284258
entrypoints_file = generate_entrypoints_file(entryPoints)
285259

286260
# Get bindings from uipath.json if available
@@ -389,28 +363,166 @@ def pack_fn(
389363
z.writestr(f"content/{file}", f.read())
390364

391365

392-
def read_toml_project(file_path: str) -> dict[str, any]:
393-
with open(file_path, "rb") as f:
394-
content = tomllib.load(f)
395-
if "project" not in content:
396-
console.error("pyproject.toml is missing the required field: project.")
397-
if "name" not in content["project"]:
398-
console.error("pyproject.toml is missing the required field: project.name.")
399-
if "description" not in content["project"]:
366+
def parse_dependency_string(dependency: str) -> Tuple[str, str]:
367+
"""Parse a dependency string into package name and version specifier.
368+
369+
Handles PEP 508 dependency specifications including:
370+
- Simple names: "requests"
371+
- Version specifiers: "requests>=2.28.0"
372+
- Complex specifiers: "requests>=2.28.0,<3.0.0"
373+
- Extras: "requests[security]>=2.28.0"
374+
- Environment markers: "requests>=2.28.0; python_version>='3.8'"
375+
376+
Args:
377+
dependency: Raw dependency string from pyproject.toml
378+
379+
Returns:
380+
Tuple of (package_name, version_specifier)
381+
382+
Examples:
383+
"requests" -> ("requests", "*")
384+
"requests>=2.28.0" -> ("requests", ">=2.28.0")
385+
"requests>=2.28.0,<3.0.0" -> ("requests", ">=2.28.0,<3.0.0")
386+
"requests[security]>=2.28.0" -> ("requests", ">=2.28.0")
387+
"""
388+
# Remove whitespace
389+
dependency = dependency.strip()
390+
391+
# Handle environment markers (everything after semicolon)
392+
if ";" in dependency:
393+
dependency = dependency.split(";")[0].strip()
394+
395+
# Pattern to match package name with optional extras and version specifiers
396+
# Matches: package_name[extras] version_specs
397+
pattern = r"^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)(\[[^\]]+\])?(.*)"
398+
match = re.match(pattern, dependency)
399+
400+
if not match:
401+
# Fallback for edge cases
402+
return dependency, "*"
403+
404+
package_name = match.group(1)
405+
version_part = match.group(4).strip() if match.group(4) else ""
406+
407+
# If no version specifier, return wildcard
408+
if not version_part:
409+
return package_name, "*"
410+
411+
# Clean up version specifier
412+
version_spec = version_part.strip()
413+
414+
# Validate that version specifier starts with a valid operator
415+
valid_operators = [">=", "<=", "==", "!=", "~=", ">", "<"]
416+
if not any(version_spec.startswith(op) for op in valid_operators):
417+
# If it doesn't start with an operator, treat as exact version
418+
if version_spec:
419+
version_spec = f"=={version_spec}"
420+
else:
421+
version_spec = "*"
422+
423+
return package_name, version_spec
424+
425+
426+
def extract_dependencies_from_toml(project_data: Dict) -> Dict[str, str]:
427+
"""Extract and parse dependencies from pyproject.toml project data.
428+
429+
Args:
430+
project_data: The "project" section from pyproject.toml
431+
432+
Returns:
433+
Dictionary mapping package names to version specifiers
434+
"""
435+
dependencies = {}
436+
437+
if "dependencies" not in project_data:
438+
return dependencies
439+
440+
deps_list = project_data["dependencies"]
441+
if not isinstance(deps_list, list):
442+
console.warning("dependencies should be a list in pyproject.toml")
443+
return dependencies
444+
445+
for dep in deps_list:
446+
if not isinstance(dep, str):
447+
console.warning(f"Skipping non-string dependency: {dep}")
448+
continue
449+
450+
try:
451+
name, version_spec = parse_dependency_string(dep)
452+
if name: # Only add if we got a valid name
453+
dependencies[name] = version_spec
454+
except Exception as e:
455+
console.warning(f"Failed to parse dependency '{dep}': {e}")
456+
continue
457+
458+
return dependencies
459+
460+
461+
def read_toml_project(file_path: str) -> dict:
462+
"""Read and parse pyproject.toml file with improved error handling and validation.
463+
464+
Args:
465+
file_path: Path to pyproject.toml file
466+
467+
Returns:
468+
Dictionary containing project metadata and dependencies
469+
"""
470+
try:
471+
with open(file_path, "rb") as f:
472+
content = tomllib.load(f)
473+
except Exception as e:
474+
console.error(f"Failed to read or parse pyproject.toml: {e}")
475+
476+
# Validate required sections
477+
if "project" not in content:
478+
console.error("pyproject.toml is missing the required field: project.")
479+
480+
project = content["project"]
481+
482+
# Validate required fields with better error messages
483+
required_fields = {
484+
"name": "Project name is required in pyproject.toml",
485+
"description": "Project description is required in pyproject.toml",
486+
"version": "Project version is required in pyproject.toml",
487+
}
488+
489+
for field, error_msg in required_fields.items():
490+
if field not in project:
400491
console.error(
401-
"pyproject.toml is missing the required field: project.description."
492+
f"pyproject.toml is missing the required field: project.{field}. {error_msg}"
402493
)
403-
if "version" not in content["project"]:
494+
495+
# Check for empty values only if field exists
496+
if field in project and (
497+
not project[field]
498+
or (isinstance(project[field], str) and not project[field].strip())
499+
):
404500
console.error(
405-
"pyproject.toml is missing the required field: project.version."
501+
f"Project {field} cannot be empty. Please specify a {field} in pyproject.toml."
406502
)
407503

408-
return {
409-
"name": content["project"]["name"],
410-
"description": content["project"]["description"],
411-
"version": content["project"]["version"],
412-
"authors": content["project"].get("authors", [{"name": ""}])[0]["name"],
413-
}
504+
# Extract author information safely
505+
authors = project.get("authors", [])
506+
author_name = ""
507+
508+
if authors and isinstance(authors, list) and len(authors) > 0:
509+
first_author = authors[0]
510+
if isinstance(first_author, dict):
511+
author_name = first_author.get("name", "")
512+
elif isinstance(first_author, str):
513+
# Handle case where authors is a list of strings
514+
author_name = first_author
515+
516+
# Extract dependencies with improved parsing
517+
dependencies = extract_dependencies_from_toml(project)
518+
519+
return {
520+
"name": project["name"].strip(),
521+
"description": project["description"].strip(),
522+
"version": project["version"].strip(),
523+
"authors": author_name.strip(),
524+
"dependencies": dependencies,
525+
}
414526

415527

416528
def get_project_version(directory):
@@ -492,6 +604,7 @@ def pack(root, nolock):
492604
version or config["version"],
493605
config["authors"],
494606
root,
607+
config.get("dependencies"),
495608
include_uv_lock=not nolock,
496609
)
497610
display_project_info(config)

0 commit comments

Comments
 (0)