Skip to content

Commit 95518e5

Browse files
authored
improve python version parsing to handle >=x,<y (#641)
* improve python version parsing to handle >=x,<y * refactor setup.py handling as well
1 parent 244eae0 commit 95518e5

File tree

2 files changed

+165
-37
lines changed

2 files changed

+165
-37
lines changed

pkg/agentfs/sdk_version_check.go

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,57 @@ func checkPackageInFile(filePath string, projectType ProjectType, pythonMinVersi
132132
return VersionCheckResult{Error: fmt.Errorf("unsupported file type: %s", fileName)}
133133
}
134134

135+
// parsePythonPackageVersion parses a Python package line and extracts the version
136+
func parsePythonPackageVersion(line string) (string, bool) {
137+
// match with optional extras and version specifiers
138+
pattern := regexp.MustCompile(`(?i)^livekit-agents(?:\[[^\]]+\])?\s*([=~><!]+)?\s*([^#]+)?`)
139+
matches := pattern.FindStringSubmatch(line)
140+
if matches == nil {
141+
return "", false
142+
}
143+
144+
// get the operator (==, >=, etc.) and the version
145+
operator := matches[1]
146+
version := strings.TrimSpace(matches[2])
147+
148+
if version == "" {
149+
// no version specified means latest
150+
return "latest", true
151+
}
152+
153+
// clean up the version string if it contains multiple constraints
154+
// handle comma-separated version constraints like ">=1.2.5,<2"
155+
if strings.Contains(version, ",") {
156+
parts := strings.Split(version, ",")
157+
for _, part := range parts {
158+
trimmed := strings.TrimSpace(part)
159+
if regexp.MustCompile(`\d`).MatchString(trimmed) {
160+
if strings.ContainsAny(trimmed, "=~><") {
161+
version = trimmed
162+
} else if operator != "" {
163+
version = operator + trimmed
164+
} else {
165+
version = trimmed
166+
}
167+
break
168+
}
169+
}
170+
} else {
171+
// handle space-separated constraints
172+
firstPart := strings.Split(version, " ")[0]
173+
// output expects operator, so we'll preserve or add it
174+
if strings.ContainsAny(firstPart, "=~><") {
175+
version = firstPart
176+
} else if operator != "" {
177+
version = operator + firstPart
178+
} else {
179+
version = firstPart
180+
}
181+
}
182+
183+
return version, true
184+
}
185+
135186
// checkRequirementsFile checks for livekit-agents in requirements.txt
136187
func checkRequirementsFile(filePath, minVersion string) VersionCheckResult {
137188
content, err := os.ReadFile(filePath)
@@ -146,15 +197,8 @@ func checkRequirementsFile(filePath, minVersion string) VersionCheckResult {
146197
continue
147198
}
148199

149-
// Match livekit-agents with optional extras and version specifiers
150-
pattern := regexp.MustCompile(`(?i)^livekit-agents(?:\[[^\]]+\])?\s*([=~><!]+)?\s*([^#\s]+)?`)
151-
matches := pattern.FindStringSubmatch(line)
152-
if matches != nil {
153-
version := strings.TrimSpace(matches[2])
154-
if version == "" {
155-
version = "latest" // No version specified means latest
156-
}
157-
200+
version, found := parsePythonPackageVersion(line)
201+
if found {
158202
satisfied, err := isVersionSatisfied(version, minVersion)
159203
return VersionCheckResult{
160204
PackageInfo: PackageInfo{
@@ -191,14 +235,8 @@ func checkPyprojectToml(filePath, minVersion string) VersionCheckResult {
191235
if deps, ok := project["dependencies"].([]any); ok {
192236
for _, dep := range deps {
193237
if line, ok := dep.(string); ok {
194-
pattern := regexp.MustCompile(`(?i)^livekit-agents(?:\[[^\]]+\])?\s*([=~><!]+)?\s*([^#\s]+)?`)
195-
matches := pattern.FindStringSubmatch(line)
196-
if matches != nil {
197-
version := strings.TrimSpace(matches[2])
198-
if version == "" {
199-
version = "latest"
200-
}
201-
238+
version, found := parsePythonPackageVersion(line)
239+
if found {
202240
satisfied, err := isVersionSatisfied(version, minVersion)
203241
return VersionCheckResult{
204242
PackageInfo: PackageInfo{
@@ -264,35 +302,37 @@ func checkSetupPy(filePath, minVersion string) VersionCheckResult {
264302

265303
// Look for install_requires or dependencies
266304
patterns := []*regexp.Regexp{
267-
regexp.MustCompile(`(?i)install_requires\s*=\s*\[(.*?)\]`),
268-
regexp.MustCompile(`(?i)dependencies\s*=\s*\[(.*?)\]`),
305+
regexp.MustCompile(`(?i)install_requires\s*=\s*\[([\s\S]*?)\]`),
306+
regexp.MustCompile(`(?i)dependencies\s*=\s*\[([\s\S]*?)\]`),
269307
}
270308

271309
for _, pattern := range patterns {
272310
matches := pattern.FindStringSubmatch(string(content))
273311
if matches != nil {
274312
depsSection := matches[1]
275-
// Parse the dependencies section
276-
depPattern := regexp.MustCompile(`(?i)["']livekit-agents(?:\[[^\]]+\])?\s*([=~><!]+)?\s*([^"'\s]+)?["']`)
313+
// extract the livekit-agents dependency line
314+
depPattern := regexp.MustCompile(`(?i)["']livekit-agents(?:\[[^\]]+\])?([^"']+)?["']`)
277315
depMatches := depPattern.FindStringSubmatch(depsSection)
278316
if depMatches != nil {
279-
version := strings.TrimSpace(depMatches[2])
280-
if version == "" {
281-
version = "latest"
317+
packageLine := "livekit-agents"
318+
if depMatches[1] != "" {
319+
packageLine += depMatches[1]
282320
}
283-
284-
satisfied, err := isVersionSatisfied(version, minVersion)
285-
return VersionCheckResult{
286-
PackageInfo: PackageInfo{
287-
Name: "livekit-agents",
288-
Version: version,
289-
FoundInFile: filePath,
290-
ProjectType: ProjectTypePythonPip,
291-
Ecosystem: "pypi",
292-
},
293-
MinVersion: minVersion,
294-
Satisfied: satisfied,
295-
Error: err,
321+
version, found := parsePythonPackageVersion(packageLine)
322+
if found {
323+
satisfied, err := isVersionSatisfied(version, minVersion)
324+
return VersionCheckResult{
325+
PackageInfo: PackageInfo{
326+
Name: "livekit-agents",
327+
Version: version,
328+
FoundInFile: filePath,
329+
ProjectType: ProjectTypePythonPip,
330+
Ecosystem: "pypi",
331+
},
332+
MinVersion: minVersion,
333+
Satisfied: satisfied,
334+
Error: err,
335+
}
296336
}
297337
}
298338
}

pkg/agentfs/sdk_version_check_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ func TestCheckSDKVersion(t *testing.T) {
2626
expectError bool
2727
errorMsg string
2828
}{
29+
{
30+
name: "Python setup.py with valid version",
31+
projectType: ProjectTypePythonPip,
32+
setupFiles: map[string]string{
33+
"setup.py": `setup(
34+
name="my-project",
35+
version="1.0.0",
36+
install_requires=[
37+
"livekit-agents>=1.5.0",
38+
"requests==2.25.1",
39+
],
40+
)`,
41+
},
42+
expectError: false,
43+
},
2944
{
3045
name: "Python requirements.txt with valid version",
3146
projectType: ProjectTypePythonPip,
@@ -52,6 +67,15 @@ dependencies = ["livekit-agents>=1.0.0"]`,
5267
},
5368
expectError: false,
5469
},
70+
{
71+
name: "Python pyproject.toml with comma-separated constraints",
72+
projectType: ProjectTypePythonPip,
73+
setupFiles: map[string]string{
74+
"pyproject.toml": `[project]
75+
dependencies = ["livekit-agents>=1.2.5,<2"]`,
76+
},
77+
expectError: false,
78+
},
5579
{
5680
name: "Python Pipfile with valid version",
5781
projectType: ProjectTypePythonPip,
@@ -351,3 +375,67 @@ func contains(s, substr string) bool {
351375
return false
352376
}())))
353377
}
378+
379+
func TestParsePythonPackageVersion(t *testing.T) {
380+
tests := []struct {
381+
name string
382+
input string
383+
expectedOutput string
384+
expectedFound bool
385+
}{
386+
{
387+
name: "Simple version",
388+
input: "livekit-agents==1.5.0",
389+
expectedOutput: "==1.5.0",
390+
expectedFound: true,
391+
},
392+
{
393+
name: "Version with extras",
394+
input: "livekit-agents[extra]==1.5.0",
395+
expectedOutput: "==1.5.0",
396+
expectedFound: true,
397+
},
398+
{
399+
name: "Version with no specifier",
400+
input: "livekit-agents",
401+
expectedOutput: "latest",
402+
expectedFound: true,
403+
},
404+
{
405+
name: "Version with greater than",
406+
input: "livekit-agents>=1.5.0",
407+
expectedOutput: ">=1.5.0",
408+
expectedFound: true,
409+
},
410+
{
411+
name: "Comma-separated constraints",
412+
input: "livekit-agents>=1.2.5,<2",
413+
expectedOutput: ">=1.2.5",
414+
expectedFound: true,
415+
},
416+
{
417+
name: "Space-separated constraints",
418+
input: "livekit-agents>=1.2.5 <2",
419+
expectedOutput: ">=1.2.5",
420+
expectedFound: true,
421+
},
422+
{
423+
name: "Not livekit-agents",
424+
input: "some-other-package==1.0.0",
425+
expectedOutput: "",
426+
expectedFound: false,
427+
},
428+
}
429+
430+
for _, tt := range tests {
431+
t.Run(tt.name, func(t *testing.T) {
432+
output, found := parsePythonPackageVersion(tt.input)
433+
if found != tt.expectedFound {
434+
t.Errorf("Expected found=%v, got %v", tt.expectedFound, found)
435+
}
436+
if output != tt.expectedOutput {
437+
t.Errorf("Expected output=%q, got %q", tt.expectedOutput, output)
438+
}
439+
})
440+
}
441+
}

0 commit comments

Comments
 (0)