From abca7848aa450044e2073d46a84dc9409048b6fa Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 28 Oct 2026 21:10:47 -0700 Subject: [PATCH 1/2] fix: detect slack-cli-hooks under any normalized python package name Python package names compare equal after PEP 503 normalization, so slack_cli_hooks, Slack.CLI-Hooks, and slack-cli-hooks all name the same package. The check for an existing slack-cli-hooks dependency in requirements.txt and pyproject.toml matched only the hyphenated spelling, which added a second slack-cli-hooks<1.0.0 entry to projects that declare slack_cli_hooks==0.3.0. https://packaging.python.org/en/latest/specifications/name-normalization/ Co-Authored-By: Claude --- internal/runtime/python/python.go | 34 ++++- internal/runtime/python/python_test.go | 197 ++++++++++++++++++++++++- 2 files changed, 228 insertions(+), 3 deletions(-) diff --git a/internal/runtime/python/python.go b/internal/runtime/python/python.go index 1d3e5a22..cb24cccb 100644 --- a/internal/runtime/python/python.go +++ b/internal/runtime/python/python.go @@ -49,6 +49,36 @@ const ( slackCLIHooksPackageSpecifier = slackCLIHooksPackageName + slackCLIHooksPackageVersion ) +// packageNameRegex matches anything shaped like a Python package name, which +// starts and ends with a letter or number with letters, numbers, ".", "_", and +// "-" between +// +// https://packaging.python.org/en/latest/specifications/name-normalization/#name-format +var packageNameRegex = regexp.MustCompile(`(?i)[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?`) + +// packageNameSeparatorRegex matches a run of the "-", "_", and "." characters +// that are treated as the same separator in Python package names +var packageNameSeparatorRegex = regexp.MustCompile(`[-_.]+`) + +// normalizePackageName returns the normalized form of a Python package name so +// names that differ only in case or separators compare as equal +// +// https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization +func normalizePackageName(name string) string { + return packageNameSeparatorRegex.ReplaceAllString(strings.ToLower(name), "-") +} + +// hasSlackCLIHooksPackage returns if slack-cli-hooks is named in the file data +// under any spelling of the package name, such as slack_cli_hooks==0.3.0 +func hasSlackCLIHooksPackage(fileData string) bool { + for _, name := range packageNameRegex.FindAllString(fileData, -1) { + if normalizePackageName(name) == slackCLIHooksPackageName { + return true + } + } + return false +} + // Python runtime type type Python struct { } @@ -188,7 +218,7 @@ func installRequirementsTxt(fs afero.Fs, projectDirPath string) (output string, fileData := string(file) // Skip when slack-cli-hooks is already declared in requirements.txt - if strings.Contains(fileData, slackCLIHooksPackageName) { + if hasSlackCLIHooksPackage(fileData) { return fmt.Sprintf("Found requirements.txt with %s", style.Highlight(slackCLIHooksPackageName)), nil } @@ -228,7 +258,7 @@ func installPyProjectToml(ctx context.Context, ios iostreams.IOStreamer, fs afer fileData := string(file) // Check if slack-cli-hooks is already declared - if strings.Contains(fileData, slackCLIHooksPackageName) { + if hasSlackCLIHooksPackage(fileData) { return fmt.Sprintf("Found pyproject.toml with %s", style.Highlight(slackCLIHooksPackageName)), nil } diff --git a/internal/runtime/python/python_test.go b/internal/runtime/python/python_test.go index 6bda436d..18954266 100644 --- a/internal/runtime/python/python_test.go +++ b/internal/runtime/python/python_test.go @@ -150,6 +150,48 @@ func Test_installRequirementsTxt(t *testing.T) { expectedOutput: "Found requirements.txt with", expectedError: false, }, + "Skip when slack-cli-hooks with a version specifier already exists": { + existingContent: "slack-bolt==1.30.0\nslack-cli-hooks==0.3.0\npytest==8.3.2", + expectedContent: "slack-bolt==1.30.0\nslack-cli-hooks==0.3.0\npytest==8.3.2", + expectedOutput: "Found requirements.txt with", + expectedError: false, + }, + "Skip when slack_cli_hooks with underscores already exists": { + existingContent: "slack_bolt==1.30.0\nslack_cli_hooks==0.3.0\npytest==8.3.2", + expectedContent: "slack_bolt==1.30.0\nslack_cli_hooks==0.3.0\npytest==8.3.2", + expectedOutput: "Found requirements.txt with", + expectedError: false, + }, + "Skip when slack.cli.hooks with dots already exists": { + existingContent: "slack-bolt==1.30.0\nslack.cli.hooks>=0.3.0\npytest==8.3.2", + expectedContent: "slack-bolt==1.30.0\nslack.cli.hooks>=0.3.0\npytest==8.3.2", + expectedOutput: "Found requirements.txt with", + expectedError: false, + }, + "Skip when Slack.CLI-Hooks with mixed case already exists": { + existingContent: "slack-bolt==1.30.0\nSlack.CLI-Hooks~=0.3\npytest==8.3.2", + expectedContent: "slack-bolt==1.30.0\nSlack.CLI-Hooks~=0.3\npytest==8.3.2", + expectedOutput: "Found requirements.txt with", + expectedError: false, + }, + "Skip when slack__cli--hooks with repeated separators already exists": { + existingContent: "slack-bolt==1.30.0\nslack__cli--hooks\npytest==8.3.2", + expectedContent: "slack-bolt==1.30.0\nslack__cli--hooks\npytest==8.3.2", + expectedOutput: "Found requirements.txt with", + expectedError: false, + }, + "Skip when slack_cli_hooks with extras and markers already exists": { + existingContent: "slack-bolt==1.30.0\nslack_cli_hooks[x]>=0.3; python_version>\"3.9\"\npytest==8.3.2", + expectedContent: "slack-bolt==1.30.0\nslack_cli_hooks[x]>=0.3; python_version>\"3.9\"\npytest==8.3.2", + expectedOutput: "Found requirements.txt with", + expectedError: false, + }, + "Add when only a package with a longer name exists": { + existingContent: "pytest==8.3.2\nslack-cli-hooks-example==1.0.0", + expectedContent: "pytest==8.3.2\nslack-cli-hooks-example==1.0.0\nslack-cli-hooks<1.0.0", + expectedOutput: "Updated requirements.txt with", + expectedError: false, + }, "Add after slack-bolt when it exists": { existingContent: "slack-bolt==2.31.2\npytest==8.3.2\nruff==0.7.2", expectedContent: "slack-bolt==2.31.2\nslack-cli-hooks<1.0.0\npytest==8.3.2\nruff==0.7.2", @@ -198,6 +240,7 @@ func Test_installRequirementsTxt(t *testing.T) { func Test_installPyProjectToml(t *testing.T) { tests := map[string]struct { existingContent string + expectedContent string shouldContain []string expectedOutput string expectedError bool @@ -206,10 +249,96 @@ func Test_installPyProjectToml(t *testing.T) { existingContent: `[project] name = "my-app" dependencies = ["slack-cli-hooks<1.0.0", "pytest==8.3.2"]`, - shouldContain: []string{"slack-cli-hooks"}, + expectedContent: `[project] +name = "my-app" +dependencies = ["slack-cli-hooks<1.0.0", "pytest==8.3.2"]`, + expectedOutput: "Found pyproject.toml with", + expectedError: false, + }, + "Skip when slack-cli-hooks with a version specifier already exists": { + existingContent: `[project] +name = "my-app" +dependencies = ["slack-bolt==1.30.0", "slack-cli-hooks==0.3.0"]`, + expectedContent: `[project] +name = "my-app" +dependencies = ["slack-bolt==1.30.0", "slack-cli-hooks==0.3.0"]`, + expectedOutput: "Found pyproject.toml with", + expectedError: false, + }, + "Skip when slack_cli_hooks with underscores already exists": { + existingContent: `[project] +name = "my-app" +dependencies = [ + "slack_bolt==1.30.0", + "slack_cli_hooks==0.3.0", + "slack_sdk==3.45.0", +]`, + expectedContent: `[project] +name = "my-app" +dependencies = [ + "slack_bolt==1.30.0", + "slack_cli_hooks==0.3.0", + "slack_sdk==3.45.0", +]`, + expectedOutput: "Found pyproject.toml with", + expectedError: false, + }, + "Skip when slack.cli.hooks with dots already exists": { + existingContent: `[project] +name = "my-app" +dependencies = ["slack-bolt==1.30.0", "slack.cli.hooks>=0.3.0"]`, + expectedContent: `[project] +name = "my-app" +dependencies = ["slack-bolt==1.30.0", "slack.cli.hooks>=0.3.0"]`, + expectedOutput: "Found pyproject.toml with", + expectedError: false, + }, + "Skip when Slack.CLI-Hooks with mixed case already exists": { + existingContent: `[project] +name = "my-app" +dependencies = ["slack-bolt==1.30.0", "Slack.CLI-Hooks~=0.3"]`, + expectedContent: `[project] +name = "my-app" +dependencies = ["slack-bolt==1.30.0", "Slack.CLI-Hooks~=0.3"]`, expectedOutput: "Found pyproject.toml with", expectedError: false, }, + "Skip when slack__cli--hooks with repeated separators already exists": { + existingContent: `[project] +name = "my-app" +dependencies = ["slack-bolt==1.30.0", "slack__cli--hooks"]`, + expectedContent: `[project] +name = "my-app" +dependencies = ["slack-bolt==1.30.0", "slack__cli--hooks"]`, + expectedOutput: "Found pyproject.toml with", + expectedError: false, + }, + "Skip when slack_cli_hooks with extras and markers already exists": { + existingContent: `[project] +name = "my-app" +dependencies = [ + "slack-bolt==1.30.0", + 'slack_cli_hooks[x]>=0.3; python_version>"3.9"', +]`, + expectedContent: `[project] +name = "my-app" +dependencies = [ + "slack-bolt==1.30.0", + 'slack_cli_hooks[x]>=0.3; python_version>"3.9"', +]`, + expectedOutput: "Found pyproject.toml with", + expectedError: false, + }, + "Add when only a package with a longer name exists": { + existingContent: `[project] +name = "my-app" +dependencies = ["slack-cli-hooks-example==1.0.0"]`, + expectedContent: `[project] +name = "my-app" +dependencies = ["slack-cli-hooks-example==1.0.0", "slack-cli-hooks<1.0.0"]`, + expectedOutput: "Updated pyproject.toml with", + expectedError: false, + }, "Add after slack-bolt when it exists": { existingContent: `[project] name = "my-app" @@ -262,6 +391,9 @@ dependencies = ["pytest==8.3.2"]`, // Verify file content contains expected strings content, err := afero.ReadFile(fs, pyprojectPath) require.NoError(t, err) + if tc.expectedContent != "" { + require.Equal(t, tc.expectedContent, string(content)) + } for _, expected := range tc.shouldContain { require.Contains(t, string(content), expected) } @@ -269,6 +401,39 @@ dependencies = ["pytest==8.3.2"]`, } } +func Test_normalizePackageName(t *testing.T) { + tests := map[string]struct { + name string + expected string + }{ + "Keeps a normalized name": { + name: "slack-cli-hooks", + expected: "slack-cli-hooks", + }, + "Replaces underscores": { + name: "slack_cli_hooks", + expected: "slack-cli-hooks", + }, + "Replaces dots": { + name: "slack.cli.hooks", + expected: "slack-cli-hooks", + }, + "Lowercases mixed case": { + name: "Slack.CLI-Hooks", + expected: "slack-cli-hooks", + }, + "Collapses runs of separators": { + name: "FrIeNdLy-._.-bArD", + expected: "friendly-bard", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + require.Equal(t, tc.expected, normalizePackageName(tc.name)) + }) + } +} + func Test_venvExists(t *testing.T) { tests := map[string]struct { venvPath string @@ -397,6 +562,16 @@ func Test_Python_InstallProjectDependencies(t *testing.T) { expectedOutputs: []string{"Found"}, expectedError: false, }, + "Skip when requirements.txt contains slack_cli_hooks==0.3.0": { + existingFiles: map[string]string{ + "requirements.txt": "slack_bolt==1.30.0\nslack_cli_hooks==0.3.0\npytest==8.3.2", + }, + expectedFiles: map[string]string{ + "requirements.txt": "slack_bolt==1.30.0\nslack_cli_hooks==0.3.0\npytest==8.3.2", + }, + expectedOutputs: []string{"Found"}, + expectedError: false, + }, "Update when requirements.txt contain slack-bolt at top of file": { existingFiles: map[string]string{ "requirements.txt": "slack-bolt==2.31.2\npytest==8.3.2\nruff==0.7.2", @@ -469,6 +644,26 @@ name = "my-app" dependencies = [ "slack-cli-hooks<1.0.0", "pytest==8.3.2", +]`, + }, + expectedOutputs: []string{"Found pyproject.toml"}, + expectedError: false, + }, + "Skip when pyproject.toml contains slack_cli_hooks": { + existingFiles: map[string]string{ + "pyproject.toml": `[project] +name = "my-app" +dependencies = [ + "slack_bolt==1.30.0", + "slack_cli_hooks==0.3.0", +]`, + }, + expectedFiles: map[string]string{ + "pyproject.toml": `[project] +name = "my-app" +dependencies = [ + "slack_bolt==1.30.0", + "slack_cli_hooks==0.3.0", ]`, }, expectedOutputs: []string{"Found pyproject.toml"}, From 7fd190704d0382be3bc6d6286e71d3ab43d72647 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 28 Oct 2026 21:22:06 -0700 Subject: [PATCH 2/2] refactor: move python package name matching to pip.go and trim tests Co-Authored-By: Claude --- internal/runtime/python/pip.go | 47 ++++++ internal/runtime/python/pip_test.go | 50 +++++++ internal/runtime/python/python.go | 34 +---- internal/runtime/python/python_test.go | 191 +------------------------ 4 files changed, 104 insertions(+), 218 deletions(-) create mode 100644 internal/runtime/python/pip.go create mode 100644 internal/runtime/python/pip_test.go diff --git a/internal/runtime/python/pip.go b/internal/runtime/python/pip.go new file mode 100644 index 00000000..e422c1d9 --- /dev/null +++ b/internal/runtime/python/pip.go @@ -0,0 +1,47 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package python + +import ( + "regexp" + "strings" +) + +// packageNameRegex matches text shaped like a Python package name +// +// https://packaging.python.org/en/latest/specifications/name-normalization/#name-format +var packageNameRegex = regexp.MustCompile(`(?i)[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?`) + +// packageNameSeparatorRegex matches runs of "-", "_", and "." which are equivalent in Python package names +var packageNameSeparatorRegex = regexp.MustCompile(`[-_.]+`) + +// normalizePackageName lowercases a Python package name and replaces runs of separators with "-" +// +// https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization +func normalizePackageName(name string) string { + return packageNameSeparatorRegex.ReplaceAllString(strings.ToLower(name), "-") +} + +// containsPackage returns if the file data names the package with any spelling +// that normalizes to the same name, such as slack_cli_hooks for slack-cli-hooks +func containsPackage(fileData string, pkgName string) bool { + pkgName = normalizePackageName(pkgName) + for _, name := range packageNameRegex.FindAllString(fileData, -1) { + if normalizePackageName(name) == pkgName { + return true + } + } + return false +} diff --git a/internal/runtime/python/pip_test.go b/internal/runtime/python/pip_test.go new file mode 100644 index 00000000..2b8e7c2b --- /dev/null +++ b/internal/runtime/python/pip_test.go @@ -0,0 +1,50 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package python + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_containsPackage(t *testing.T) { + tests := map[string]struct { + fileData string + expected bool + }{ + "Matches the normalized name": { + fileData: "slack-cli-hooks<1.0.0", + expected: true, + }, + "Matches dots and mixed case": { + fileData: "Slack.CLI-Hooks", + expected: true, + }, + "Matches underscores with extras and markers": { + fileData: `slack_cli_hooks[x]>=0.3; python_version>"3.9"`, + expected: true, + }, + "Skips a longer package name": { + fileData: "slack-cli-hooks-example==1.0.0", + expected: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + require.Equal(t, tc.expected, containsPackage(tc.fileData, "slack-cli-hooks")) + }) + } +} diff --git a/internal/runtime/python/python.go b/internal/runtime/python/python.go index cb24cccb..bfcc973b 100644 --- a/internal/runtime/python/python.go +++ b/internal/runtime/python/python.go @@ -49,36 +49,6 @@ const ( slackCLIHooksPackageSpecifier = slackCLIHooksPackageName + slackCLIHooksPackageVersion ) -// packageNameRegex matches anything shaped like a Python package name, which -// starts and ends with a letter or number with letters, numbers, ".", "_", and -// "-" between -// -// https://packaging.python.org/en/latest/specifications/name-normalization/#name-format -var packageNameRegex = regexp.MustCompile(`(?i)[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?`) - -// packageNameSeparatorRegex matches a run of the "-", "_", and "." characters -// that are treated as the same separator in Python package names -var packageNameSeparatorRegex = regexp.MustCompile(`[-_.]+`) - -// normalizePackageName returns the normalized form of a Python package name so -// names that differ only in case or separators compare as equal -// -// https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization -func normalizePackageName(name string) string { - return packageNameSeparatorRegex.ReplaceAllString(strings.ToLower(name), "-") -} - -// hasSlackCLIHooksPackage returns if slack-cli-hooks is named in the file data -// under any spelling of the package name, such as slack_cli_hooks==0.3.0 -func hasSlackCLIHooksPackage(fileData string) bool { - for _, name := range packageNameRegex.FindAllString(fileData, -1) { - if normalizePackageName(name) == slackCLIHooksPackageName { - return true - } - } - return false -} - // Python runtime type type Python struct { } @@ -218,7 +188,7 @@ func installRequirementsTxt(fs afero.Fs, projectDirPath string) (output string, fileData := string(file) // Skip when slack-cli-hooks is already declared in requirements.txt - if hasSlackCLIHooksPackage(fileData) { + if containsPackage(fileData, slackCLIHooksPackageName) { return fmt.Sprintf("Found requirements.txt with %s", style.Highlight(slackCLIHooksPackageName)), nil } @@ -258,7 +228,7 @@ func installPyProjectToml(ctx context.Context, ios iostreams.IOStreamer, fs afer fileData := string(file) // Check if slack-cli-hooks is already declared - if hasSlackCLIHooksPackage(fileData) { + if containsPackage(fileData, slackCLIHooksPackageName) { return fmt.Sprintf("Found pyproject.toml with %s", style.Highlight(slackCLIHooksPackageName)), nil } diff --git a/internal/runtime/python/python_test.go b/internal/runtime/python/python_test.go index 18954266..092fd137 100644 --- a/internal/runtime/python/python_test.go +++ b/internal/runtime/python/python_test.go @@ -150,48 +150,12 @@ func Test_installRequirementsTxt(t *testing.T) { expectedOutput: "Found requirements.txt with", expectedError: false, }, - "Skip when slack-cli-hooks with a version specifier already exists": { - existingContent: "slack-bolt==1.30.0\nslack-cli-hooks==0.3.0\npytest==8.3.2", - expectedContent: "slack-bolt==1.30.0\nslack-cli-hooks==0.3.0\npytest==8.3.2", - expectedOutput: "Found requirements.txt with", - expectedError: false, - }, - "Skip when slack_cli_hooks with underscores already exists": { + "Skip when slack_cli_hooks already exists": { existingContent: "slack_bolt==1.30.0\nslack_cli_hooks==0.3.0\npytest==8.3.2", expectedContent: "slack_bolt==1.30.0\nslack_cli_hooks==0.3.0\npytest==8.3.2", expectedOutput: "Found requirements.txt with", expectedError: false, }, - "Skip when slack.cli.hooks with dots already exists": { - existingContent: "slack-bolt==1.30.0\nslack.cli.hooks>=0.3.0\npytest==8.3.2", - expectedContent: "slack-bolt==1.30.0\nslack.cli.hooks>=0.3.0\npytest==8.3.2", - expectedOutput: "Found requirements.txt with", - expectedError: false, - }, - "Skip when Slack.CLI-Hooks with mixed case already exists": { - existingContent: "slack-bolt==1.30.0\nSlack.CLI-Hooks~=0.3\npytest==8.3.2", - expectedContent: "slack-bolt==1.30.0\nSlack.CLI-Hooks~=0.3\npytest==8.3.2", - expectedOutput: "Found requirements.txt with", - expectedError: false, - }, - "Skip when slack__cli--hooks with repeated separators already exists": { - existingContent: "slack-bolt==1.30.0\nslack__cli--hooks\npytest==8.3.2", - expectedContent: "slack-bolt==1.30.0\nslack__cli--hooks\npytest==8.3.2", - expectedOutput: "Found requirements.txt with", - expectedError: false, - }, - "Skip when slack_cli_hooks with extras and markers already exists": { - existingContent: "slack-bolt==1.30.0\nslack_cli_hooks[x]>=0.3; python_version>\"3.9\"\npytest==8.3.2", - expectedContent: "slack-bolt==1.30.0\nslack_cli_hooks[x]>=0.3; python_version>\"3.9\"\npytest==8.3.2", - expectedOutput: "Found requirements.txt with", - expectedError: false, - }, - "Add when only a package with a longer name exists": { - existingContent: "pytest==8.3.2\nslack-cli-hooks-example==1.0.0", - expectedContent: "pytest==8.3.2\nslack-cli-hooks-example==1.0.0\nslack-cli-hooks<1.0.0", - expectedOutput: "Updated requirements.txt with", - expectedError: false, - }, "Add after slack-bolt when it exists": { existingContent: "slack-bolt==2.31.2\npytest==8.3.2\nruff==0.7.2", expectedContent: "slack-bolt==2.31.2\nslack-cli-hooks<1.0.0\npytest==8.3.2\nruff==0.7.2", @@ -240,7 +204,6 @@ func Test_installRequirementsTxt(t *testing.T) { func Test_installPyProjectToml(t *testing.T) { tests := map[string]struct { existingContent string - expectedContent string shouldContain []string expectedOutput string expectedError bool @@ -249,96 +212,18 @@ func Test_installPyProjectToml(t *testing.T) { existingContent: `[project] name = "my-app" dependencies = ["slack-cli-hooks<1.0.0", "pytest==8.3.2"]`, - expectedContent: `[project] -name = "my-app" -dependencies = ["slack-cli-hooks<1.0.0", "pytest==8.3.2"]`, - expectedOutput: "Found pyproject.toml with", - expectedError: false, - }, - "Skip when slack-cli-hooks with a version specifier already exists": { - existingContent: `[project] -name = "my-app" -dependencies = ["slack-bolt==1.30.0", "slack-cli-hooks==0.3.0"]`, - expectedContent: `[project] -name = "my-app" -dependencies = ["slack-bolt==1.30.0", "slack-cli-hooks==0.3.0"]`, - expectedOutput: "Found pyproject.toml with", - expectedError: false, - }, - "Skip when slack_cli_hooks with underscores already exists": { - existingContent: `[project] -name = "my-app" -dependencies = [ - "slack_bolt==1.30.0", - "slack_cli_hooks==0.3.0", - "slack_sdk==3.45.0", -]`, - expectedContent: `[project] -name = "my-app" -dependencies = [ - "slack_bolt==1.30.0", - "slack_cli_hooks==0.3.0", - "slack_sdk==3.45.0", -]`, - expectedOutput: "Found pyproject.toml with", - expectedError: false, - }, - "Skip when slack.cli.hooks with dots already exists": { - existingContent: `[project] -name = "my-app" -dependencies = ["slack-bolt==1.30.0", "slack.cli.hooks>=0.3.0"]`, - expectedContent: `[project] -name = "my-app" -dependencies = ["slack-bolt==1.30.0", "slack.cli.hooks>=0.3.0"]`, + shouldContain: []string{"slack-cli-hooks"}, expectedOutput: "Found pyproject.toml with", expectedError: false, }, - "Skip when Slack.CLI-Hooks with mixed case already exists": { + "Skip when slack_cli_hooks already exists": { existingContent: `[project] name = "my-app" -dependencies = ["slack-bolt==1.30.0", "Slack.CLI-Hooks~=0.3"]`, - expectedContent: `[project] -name = "my-app" -dependencies = ["slack-bolt==1.30.0", "Slack.CLI-Hooks~=0.3"]`, +dependencies = ["slack_bolt==1.30.0", "slack_cli_hooks==0.3.0"]`, + shouldContain: []string{"slack_cli_hooks==0.3.0"}, expectedOutput: "Found pyproject.toml with", expectedError: false, }, - "Skip when slack__cli--hooks with repeated separators already exists": { - existingContent: `[project] -name = "my-app" -dependencies = ["slack-bolt==1.30.0", "slack__cli--hooks"]`, - expectedContent: `[project] -name = "my-app" -dependencies = ["slack-bolt==1.30.0", "slack__cli--hooks"]`, - expectedOutput: "Found pyproject.toml with", - expectedError: false, - }, - "Skip when slack_cli_hooks with extras and markers already exists": { - existingContent: `[project] -name = "my-app" -dependencies = [ - "slack-bolt==1.30.0", - 'slack_cli_hooks[x]>=0.3; python_version>"3.9"', -]`, - expectedContent: `[project] -name = "my-app" -dependencies = [ - "slack-bolt==1.30.0", - 'slack_cli_hooks[x]>=0.3; python_version>"3.9"', -]`, - expectedOutput: "Found pyproject.toml with", - expectedError: false, - }, - "Add when only a package with a longer name exists": { - existingContent: `[project] -name = "my-app" -dependencies = ["slack-cli-hooks-example==1.0.0"]`, - expectedContent: `[project] -name = "my-app" -dependencies = ["slack-cli-hooks-example==1.0.0", "slack-cli-hooks<1.0.0"]`, - expectedOutput: "Updated pyproject.toml with", - expectedError: false, - }, "Add after slack-bolt when it exists": { existingContent: `[project] name = "my-app" @@ -391,9 +276,6 @@ dependencies = ["pytest==8.3.2"]`, // Verify file content contains expected strings content, err := afero.ReadFile(fs, pyprojectPath) require.NoError(t, err) - if tc.expectedContent != "" { - require.Equal(t, tc.expectedContent, string(content)) - } for _, expected := range tc.shouldContain { require.Contains(t, string(content), expected) } @@ -401,39 +283,6 @@ dependencies = ["pytest==8.3.2"]`, } } -func Test_normalizePackageName(t *testing.T) { - tests := map[string]struct { - name string - expected string - }{ - "Keeps a normalized name": { - name: "slack-cli-hooks", - expected: "slack-cli-hooks", - }, - "Replaces underscores": { - name: "slack_cli_hooks", - expected: "slack-cli-hooks", - }, - "Replaces dots": { - name: "slack.cli.hooks", - expected: "slack-cli-hooks", - }, - "Lowercases mixed case": { - name: "Slack.CLI-Hooks", - expected: "slack-cli-hooks", - }, - "Collapses runs of separators": { - name: "FrIeNdLy-._.-bArD", - expected: "friendly-bard", - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - require.Equal(t, tc.expected, normalizePackageName(tc.name)) - }) - } -} - func Test_venvExists(t *testing.T) { tests := map[string]struct { venvPath string @@ -562,16 +411,6 @@ func Test_Python_InstallProjectDependencies(t *testing.T) { expectedOutputs: []string{"Found"}, expectedError: false, }, - "Skip when requirements.txt contains slack_cli_hooks==0.3.0": { - existingFiles: map[string]string{ - "requirements.txt": "slack_bolt==1.30.0\nslack_cli_hooks==0.3.0\npytest==8.3.2", - }, - expectedFiles: map[string]string{ - "requirements.txt": "slack_bolt==1.30.0\nslack_cli_hooks==0.3.0\npytest==8.3.2", - }, - expectedOutputs: []string{"Found"}, - expectedError: false, - }, "Update when requirements.txt contain slack-bolt at top of file": { existingFiles: map[string]string{ "requirements.txt": "slack-bolt==2.31.2\npytest==8.3.2\nruff==0.7.2", @@ -644,26 +483,6 @@ name = "my-app" dependencies = [ "slack-cli-hooks<1.0.0", "pytest==8.3.2", -]`, - }, - expectedOutputs: []string{"Found pyproject.toml"}, - expectedError: false, - }, - "Skip when pyproject.toml contains slack_cli_hooks": { - existingFiles: map[string]string{ - "pyproject.toml": `[project] -name = "my-app" -dependencies = [ - "slack_bolt==1.30.0", - "slack_cli_hooks==0.3.0", -]`, - }, - expectedFiles: map[string]string{ - "pyproject.toml": `[project] -name = "my-app" -dependencies = [ - "slack_bolt==1.30.0", - "slack_cli_hooks==0.3.0", ]`, }, expectedOutputs: []string{"Found pyproject.toml"},