mirror of https://github.com/databricks/cli.git
Extend "notebook not found" error to warn about missing extension (#1920)
## Changes The full workspace path for a notebook does not contain the notebook's extension. If a user converts that file path to a relative path (like `/Workspace/bundle_root/bar/nb` -> `./bar/nb`), they can be confused as to why the new file path does not work. The changes in this PR nudge them to add the appropriate file extension (e.g., `./bar/nb.py` or `./bar/nb.ipynb`). One common way users can end up in this scenario is by using the view job as YAML functionality in the Databricks UI. ## Tests Unit test and manually. ``` (.venv) ➜ bundle-playground git:(master) ✗ cli bundle validate Error: notebook ./foo not found. Local notebook references are expected to contain one of the following file extensions: [.py, .r, .scala, .sql, .ipynb] ```
This commit is contained in:
parent
14fe03dcb9
commit
c2e2abcc35
|
@ -126,8 +126,34 @@ func (t *translateContext) rewritePath(
|
||||||
func (t *translateContext) translateNotebookPath(literal, localFullPath, localRelPath, remotePath string) (string, error) {
|
func (t *translateContext) translateNotebookPath(literal, localFullPath, localRelPath, remotePath string) (string, error) {
|
||||||
nb, _, err := notebook.DetectWithFS(t.b.SyncRoot, filepath.ToSlash(localRelPath))
|
nb, _, err := notebook.DetectWithFS(t.b.SyncRoot, filepath.ToSlash(localRelPath))
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
if filepath.Ext(localFullPath) != notebook.ExtensionNone {
|
||||||
return "", fmt.Errorf("notebook %s not found", literal)
|
return "", fmt.Errorf("notebook %s not found", literal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extensions := []string{
|
||||||
|
notebook.ExtensionPython,
|
||||||
|
notebook.ExtensionR,
|
||||||
|
notebook.ExtensionScala,
|
||||||
|
notebook.ExtensionSql,
|
||||||
|
notebook.ExtensionJupyter,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether a file with a notebook extension already exists. This
|
||||||
|
// way we can provide a more targeted error message.
|
||||||
|
for _, ext := range extensions {
|
||||||
|
literalWithExt := literal + ext
|
||||||
|
localRelPathWithExt := filepath.ToSlash(localRelPath + ext)
|
||||||
|
if _, err := fs.Stat(t.b.SyncRoot, localRelPathWithExt); err == nil {
|
||||||
|
return "", fmt.Errorf(`notebook %s not found. Did you mean %s?
|
||||||
|
Local notebook references are expected to contain one of the following
|
||||||
|
file extensions: [%s]`, literal, literalWithExt, strings.Join(extensions, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a generic error message if no matching possible file is found.
|
||||||
|
return "", fmt.Errorf(`notebook %s not found. Local notebook references are expected
|
||||||
|
to contain one of the following file extensions: [%s]`, literal, strings.Join(extensions, ", "))
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to determine if %s is a notebook: %w", localFullPath, err)
|
return "", fmt.Errorf("unable to determine if %s is a notebook: %w", localFullPath, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package mutator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -508,6 +509,59 @@ func TestPipelineNotebookDoesNotExistError(t *testing.T) {
|
||||||
assert.EqualError(t, diags.Error(), "notebook ./doesnt_exist.py not found")
|
assert.EqualError(t, diags.Error(), "notebook ./doesnt_exist.py not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPipelineNotebookDoesNotExistErrorWithoutExtension(t *testing.T) {
|
||||||
|
for _, ext := range []string{
|
||||||
|
".py",
|
||||||
|
".r",
|
||||||
|
".scala",
|
||||||
|
".sql",
|
||||||
|
".ipynb",
|
||||||
|
"",
|
||||||
|
} {
|
||||||
|
t.Run("case_"+ext, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
if ext != "" {
|
||||||
|
touchEmptyFile(t, filepath.Join(dir, "foo"+ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
SyncRootPath: dir,
|
||||||
|
SyncRoot: vfs.MustNew(dir),
|
||||||
|
Config: config.Root{
|
||||||
|
Resources: config.Resources{
|
||||||
|
Pipelines: map[string]*resources.Pipeline{
|
||||||
|
"pipeline": {
|
||||||
|
PipelineSpec: &pipelines.PipelineSpec{
|
||||||
|
Libraries: []pipelines.PipelineLibrary{
|
||||||
|
{
|
||||||
|
Notebook: &pipelines.NotebookLibrary{
|
||||||
|
Path: "./foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bundletest.SetLocation(b, ".", []dyn.Location{{File: filepath.Join(dir, "fake.yml")}})
|
||||||
|
diags := bundle.Apply(context.Background(), b, mutator.TranslatePaths())
|
||||||
|
|
||||||
|
if ext == "" {
|
||||||
|
assert.EqualError(t, diags.Error(), `notebook ./foo not found. Local notebook references are expected
|
||||||
|
to contain one of the following file extensions: [.py, .r, .scala, .sql, .ipynb]`)
|
||||||
|
} else {
|
||||||
|
assert.EqualError(t, diags.Error(), fmt.Sprintf(`notebook ./foo not found. Did you mean ./foo%s?
|
||||||
|
Local notebook references are expected to contain one of the following
|
||||||
|
file extensions: [.py, .r, .scala, .sql, .ipynb]`, ext))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPipelineFileDoesNotExistError(t *testing.T) {
|
func TestPipelineFileDoesNotExistError(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue