Skip to content
Open
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
1 change: 1 addition & 0 deletions .nextchanges/cli/setup-local-orthogonal-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added orthogonal `--no-constraints` and `--no-dbconnect` flags to `databricks environments setup-local`. The flags compose: `--no-constraints` skips writing the remote Python-version and dependency pins, and `--no-dbconnect` skips the databricks-connect dependency.
2 changes: 2 additions & 0 deletions acceptance/localenv/help/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Flags:
--dry-run compute the plan without writing files or provisioning
-h, --help help for setup-local
--job-task string job task to use as the compute target, as <job-id>.<task-key> (the task key is required)
--no-constraints skip writing the remote Python version and dependency constraints
--no-dbconnect skip adding the databricks-connect dependency
--serverless-version string serverless version to use as the compute target (e.g. 5)

Global Flags:
Expand Down
2 changes: 2 additions & 0 deletions acceptance/localenv/no-constraints/out.test.toml

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

60 changes: 60 additions & 0 deletions acceptance/localenv/no-constraints/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

>>> [CLI] environments setup-local --serverless-version 4 --no-constraints --dry-run --output json
{
"schemaVersion": 1,
"command": "environments setup-local",
"ok": true,
"mode": "default",
"dryRun": true,
"compute": {
"source": "serverless",
"serverlessVersion": "v4",
"envKey": "serverless/serverless-v4"
},
"resolved": {
"pythonVersion": "3.12",
"dbconnectVersion": "17.2.0",
"artifactSource": "network"
},
"greenfield": false,
"plan": {
"wouldWrite": "[TEST_TMP_DIR]/pyproject.toml",
"wouldBackup": "[TEST_TMP_DIR]/pyproject.toml.bak",
"wouldInstallPython": "3.12",
"diff": "--- pyproject.toml\n+++ pyproject.toml.new\n@@ -3,4 +3,7 @@\n requires-python = \"\u003e=3.10\"\n \n [dependency-groups]\n-dev = [\"databricks-connect~=16.0\"]\n+dev = [\"databricks-connect~=17.2.0\"]\n+\n+[tool.databricks.environment]\n+environment_version = \"4\"\n"
},
"phases": [
{
"phase": "preflight",
"status": "ok"
},
{
"phase": "resolve",
"status": "ok"
},
{
"phase": "fetch",
"status": "ok"
},
{
"phase": "merge",
"status": "ok"
},
{
"phase": "provision",
"status": "ok"
},
{
"phase": "validate",
"status": "ok"
}
],
"warnings": [
{
"code": "W_DBCONNECT_PIN_OVERRIDDEN",
"message": "databricks-connect \"databricks-connect~=16.0\" is replaced by the environment's \"databricks-connect~=17.2.0\""
}
],
"error": null,
"durationMs": [DURATION_MS]
}
15 changes: 15 additions & 0 deletions acceptance/localenv/no-constraints/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# --no-constraints leaves the remote Python version and dependency pins
# unmanaged: the plan writes neither requires-python nor the [tool.uv] constraint
# block, and the user's existing requires-python is left untouched. The
# databricks-connect dependency (orthogonal to --no-constraints) is still managed.
# The JSON plan shows the diff.
cat > pyproject.toml <<'PY'
[project]
name = "demo"
requires-python = ">=3.10"

[dependency-groups]
dev = ["databricks-connect~=16.0"]
PY

trace $CLI environments setup-local --serverless-version 4 --no-constraints --dry-run --output json
23 changes: 23 additions & 0 deletions acceptance/localenv/no-constraints/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

# The script writes pyproject.toml as the merge input; --dry-run leaves it unchanged.
Ignore = ["pyproject.toml"]

Env.DATABRICKS_LOCALENV_CONSTRAINT_SOURCE_URL_TEST_OVERRIDE = "$DATABRICKS_HOST"

[[Server]]
Pattern = "GET /serverless/serverless-v4/pyproject.toml"
Response.Body = '''
[project]
requires-python = ">=3.12"

[dependency-groups]
dev = ["databricks-connect~=17.2.0"]

[tool.uv]
constraint-dependencies = ["pyarrow<19"]
'''

[[Repls]]
Old = 'uv uv \S+(?: \([^)]+\))?'
New = 'uv [UV_VERSION]'
2 changes: 2 additions & 0 deletions acceptance/localenv/no-dbconnect/out.test.toml

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

59 changes: 59 additions & 0 deletions acceptance/localenv/no-dbconnect/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@

>>> [CLI] environments setup-local --serverless-version 4 --no-dbconnect --dry-run --output json
{
"schemaVersion": 1,
"command": "environments setup-local",
"ok": true,
"mode": "constraints-only",
"dryRun": true,
"compute": {
"source": "serverless",
"serverlessVersion": "v4",
"envKey": "serverless/serverless-v4"
},
"resolved": {
"pythonVersion": "3.12",
"artifactSource": "network"
},
"greenfield": false,
"plan": {
"wouldWrite": "[TEST_TMP_DIR]/pyproject.toml",
"wouldBackup": "[TEST_TMP_DIR]/pyproject.toml.bak",
"wouldInstallPython": "3.12",
"diff": "--- pyproject.toml\n+++ pyproject.toml.new\n@@ -1,7 +1,17 @@\n [project]\n name = \"demo\"\n-requires-python = \"\u003e=3.10\"\n+requires-python = \"\u003e=3.12\"\n dependencies = [\"databricks-connect==15.1.*\"]\n \n [dependency-groups]\n dev = [\"databricks-connect~=16.0\"]\n+\n+[tool.databricks.environment]\n+environment_version = \"4\"\n+\n+# managed by databricks environments setup-local — do not edit\n+[tool.uv]\n+constraint-dependencies = [\n+ \"pyarrow\u003c19\",\n+]\n+# end managed by databricks environments setup-local\n"
},
"phases": [
{
"phase": "preflight",
"status": "ok"
},
{
"phase": "resolve",
"status": "ok"
},
{
"phase": "fetch",
"status": "ok"
},
{
"phase": "merge",
"status": "ok"
},
{
"phase": "provision",
"status": "ok"
},
{
"phase": "validate",
"status": "ok"
}
],
"warnings": [
{
"code": "W_REQUIRES_PYTHON_OVERRIDDEN",
"message": "requires-python \"\u003e=3.10\" is replaced by the environment's \"\u003e=3.12\""
}
],
"error": null,
"durationMs": [DURATION_MS]
}
15 changes: 15 additions & 0 deletions acceptance/localenv/no-dbconnect/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# --no-dbconnect is the orthogonal spelling of --constraints-only: it omits the
# databricks-connect dependency (mode "constraints-only") while still managing
# requires-python and the [tool.uv] constraints. Existing databricks-connect
# requirements the user already had are left untouched.
cat > pyproject.toml <<'PY'
[project]
name = "demo"
requires-python = ">=3.10"
dependencies = ["databricks-connect==15.1.*"]

[dependency-groups]
dev = ["databricks-connect~=16.0"]
PY

trace $CLI environments setup-local --serverless-version 4 --no-dbconnect --dry-run --output json
23 changes: 23 additions & 0 deletions acceptance/localenv/no-dbconnect/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

# The script writes pyproject.toml as the merge input; --dry-run leaves it unchanged.
Ignore = ["pyproject.toml"]

Env.DATABRICKS_LOCALENV_CONSTRAINT_SOURCE_URL_TEST_OVERRIDE = "$DATABRICKS_HOST"

[[Server]]
Pattern = "GET /serverless/serverless-v4/pyproject.toml"
Response.Body = '''
[project]
requires-python = ">=3.12"

[dependency-groups]
dev = ["databricks-connect~=17.2.0"]

[tool.uv]
constraint-dependencies = ["pyarrow<19"]
'''

[[Repls]]
Old = 'uv uv \S+(?: \([^)]+\))?'
New = 'uv [UV_VERSION]'
2 changes: 1 addition & 1 deletion cmd/environments/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func renderResult(ctx context.Context, cmd *cobra.Command, res *libslocalenv.Res
return nil
}

// renderSuccess prints the friendly post-provision summary (DECO-27977).
// renderSuccess prints the friendly post-provision summary.
//
// It runs only on a non-dry-run success (renderResult returns earlier for JSON,
// failures, and dry runs), so res.VenvPath is always set: the validate phase — the
Expand Down
12 changes: 11 additions & 1 deletion cmd/environments/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ func addComputeFlags(cmd *cobra.Command) {
cmd.Flags().String("serverless-version", "", "serverless version to use as the compute target (e.g. 5)")
cmd.Flags().String("job-task", "", "job task to use as the compute target, as <job-id>.<task-key> (the task key is required)")
cmd.Flags().Bool("constraints-only", false, "apply the Python version and constraints without adding the databricks-connect dependency")
// The negative flags (--no-constraints, --no-dbconnect) are orthogonal and
// compose. --no-dbconnect and the older --constraints-only are equivalent (both
// skip the databricks-connect dependency).
cmd.Flags().Bool("no-constraints", false, "skip writing the remote Python version and dependency constraints")
cmd.Flags().Bool("no-dbconnect", false, "skip adding the databricks-connect dependency")
cmd.Flags().Bool("dry-run", false, "compute the plan without writing files or provisioning")
// The mutual exclusivity of the target flags is enforced in the pipeline's
// preflight (as E_USAGE) rather than via cmd.MarkFlagsMutuallyExclusive, so
Expand Down Expand Up @@ -122,6 +127,8 @@ func runPipeline(cmd *cobra.Command) error {
serverless, _ := cmd.Flags().GetString("serverless-version")
jobTask, _ := cmd.Flags().GetString("job-task")
constraintsOnly, _ := cmd.Flags().GetBool("constraints-only")
noConstraints, _ := cmd.Flags().GetBool("no-constraints")
noDBConnect, _ := cmd.Flags().GetBool("no-dbconnect")
check, _ := cmd.Flags().GetBool("dry-run")

computeFlags := libslocalenv.ComputeFlags{
Expand All @@ -134,8 +141,10 @@ func runPipeline(cmd *cobra.Command) error {
// preflight, so a conflict is reported as E_USAGE through the phase/JSON
// contract rather than as a bare error here.

// --no-dbconnect is the orthogonal spelling of --constraints-only; either skips
// the databricks-connect dependency, which the pipeline models as the mode.
mode := libslocalenv.ModeDefault
if constraintsOnly {
if constraintsOnly || noDBConnect {
mode = libslocalenv.ModeConstraintsOnly
}

Expand Down Expand Up @@ -177,6 +186,7 @@ func runPipeline(cmd *cobra.Command) error {
p := &libslocalenv.Pipeline{
Mode: mode,
Check: check,
SkipConstraints: noConstraints,
ProjectDir: projectDir,
ConstraintBaseURL: constraintBaseURL,
CacheDir: cacheDir,
Expand Down
8 changes: 8 additions & 0 deletions libs/localenv/constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,15 @@ func parseConstraints(data []byte) (requiresPython, dbconnect string, deps []str
}
}

// Normalize a missing [tool.uv].constraint-dependencies to a non-nil empty
// slice. A nil ConstraintDeps is reserved as the --no-constraints "leave the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not thread the --no-constraints flag through so we don't need a special signal value? (same is true for other signal values used elsewhere)

// constraint block unmanaged" signal (mergeToolUv and RenderFreshPyproject skip
// on nil); without this, an artifact that simply omits the key would be
// indistinguishable from the flag and would silently stop being managed.
deps = p.Tool.UV.ConstraintDependencies
if deps == nil {
deps = []string{}
}
return requiresPython, dbconnect, deps, nil
}

Expand Down
17 changes: 17 additions & 0 deletions libs/localenv/constraints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,20 @@ func TestFetchConstraintsUnusableBodyDoesNotPoisonCache(t *testing.T) {
assert.True(t, c.FromCache)
assert.Equal(t, "==3.12.*", c.RequiresPython)
}

func TestParseConstraintsNormalizesMissingConstraintDepsToEmpty(t *testing.T) {
// An artifact without [tool.uv].constraint-dependencies yields a non-nil empty
// slice, not nil, so a nil ConstraintDeps is reserved as the --no-constraints
// "leave the constraint block unmanaged" signal (mergeToolUv / RenderFreshPyproject
// treat nil as skip). Without this, a normal artifact that simply omits the key
// would be indistinguishable from the flag.
_, _, deps, err := parseConstraints([]byte(`[project]
requires-python = ">=3.12"

[dependency-groups]
dev = ["databricks-connect~=17.2.0"]
`))
require.NoError(t, err)
require.NotNil(t, deps)
assert.Empty(t, deps)
}
34 changes: 30 additions & 4 deletions libs/localenv/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ func tableBounds(lines []string, name string) (header, end int, found bool) {
// the line's leading whitespace. If the key is absent, it is inserted directly under the
// [project] header. Returns whether the line slice changed.
func mergeRequiresPython(lines []string, value string) ([]string, bool) {
// An empty value means requires-python is unmanaged (--no-constraints): leave
// the user's line untouched rather than overwrite it with a blank pin. This
// mirrors how an empty DatabricksConnect / EnvironmentVersion is a no-op; a
// real fetched artifact always carries a requires-python, so empty only ever
// reaches here when the caller deliberately cleared it.
if value == "" {
return lines, false
}

header, end, found := tableBounds(lines, "[project]")
if !found {
return lines, false
Expand Down Expand Up @@ -861,6 +870,15 @@ func arrayLineSpan(lines []string, start, limit int) (last int, multiline bool)
// marker-bracketed block already exists, its contents are replaced in place. Otherwise any
// plain [tool.uv] table is removed and a fresh marker-bracketed block is appended at EOF.
func mergeToolUv(lines, deps []string) ([]string, bool) {
// A nil deps slice means the [tool.uv] constraint region is unmanaged
// (--no-constraints): leave any existing block untouched and write none.
// Distinct from a non-nil empty slice, which still renders an empty managed
// block; a real fetched artifact always carries constraint-dependencies, so
// nil only reaches here when the caller deliberately cleared it.
if deps == nil {
return lines, false
}

start, stop, found := markerBounds(lines)
if found {
// Replace the existing managed region in place. Whether it owns a [tool.uv]
Expand Down Expand Up @@ -1232,7 +1250,11 @@ func RenderFreshPyproject(projectName string, c Constraints) []byte {
fmt.Fprintf(&b, "name = %q\n", projectName)
// uv requires project.version when a [project] table is present.
fmt.Fprintf(&b, "version = %q\n", freshProjectVersion)
fmt.Fprintf(&b, "requires-python = %q\n", c.RequiresPython)
// requires-python is omitted when unmanaged (--no-constraints); an empty pin
// would be invalid, and a fresh project without it lets uv pick the interpreter.
if c.RequiresPython != "" {
fmt.Fprintf(&b, "requires-python = %q\n", c.RequiresPython)
}
b.WriteString("\n")
b.WriteString("[dependency-groups]\n")
if c.DatabricksConnect != "" {
Expand All @@ -1250,9 +1272,13 @@ func RenderFreshPyproject(projectName string, c Constraints) []byte {
fmt.Fprintf(&b, "environment_version = %q\n", c.EnvironmentVersion)
b.WriteString("\n")
}
for _, line := range renderToolUvBlock(c.ConstraintDeps, true) {
b.WriteString(line)
b.WriteString("\n")
// The [tool.uv] constraint block is omitted when unmanaged (--no-constraints,
// signalled by a nil slice); a non-nil empty slice still renders an empty block.
if c.ConstraintDeps != nil {
for _, line := range renderToolUvBlock(c.ConstraintDeps, true) {
b.WriteString(line)
b.WriteString("\n")
}
}
return []byte(b.String())
}
Loading
Loading