2024-07-02 15:10:53 +00:00
|
|
|
package python
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
|
|
|
|
"github.com/databricks/cli/libs/diag"
|
|
|
|
"github.com/databricks/cli/libs/dyn"
|
|
|
|
)
|
|
|
|
|
PythonMutator: propagate source locations (#1783)
## Changes
Add a mechanism to load Python source locations in the Python mutator.
Previously, locations pointed to generated YAML. Now, they point to
Python sources instead. Python process outputs "locations.json"
containing locations of bundle paths, examples:
```json
{"path": "resources.jobs.job_0", "file": "resources/job_0.py", "line": 3, "column": 5}
{"path": "resources.jobs.job_0.tasks[0].task_key", "file": "resources/job_0.py", "line": 10, "column": 5}
{"path": "resources.jobs.job_1", "file": "resources/job_1.py", "line": 5, "column": 7}
```
Such locations form a tree, and we assign locations of the closest
ancestor to each `dyn.Value` based on its path. For example,
`resources.jobs.job_0.tasks[0].task_key` is located at `job_0.py:10:5`
and `resources.jobs.job_0.tasks[0].email_notifications` is located at
`job_0.py:3:5`, because we use the location of the job as the most
precise approximation.
This feature is only enabled if `experimental/python` is used.
Note: for now, we don't update locations with relative paths, because it
has a side effect in changing how these paths are resolved
## Example
```
% databricks bundle validate
Warning: job_cluster_key abc is not defined
at resources.jobs.examples.tasks[0].job_cluster_key
in resources/example.py:10:1
```
## Tests
Unit tests and manually
2025-01-22 15:37:37 +00:00
|
|
|
// pythonDiagnostic is a single entry in diagnostics.json
|
2024-07-02 15:10:53 +00:00
|
|
|
type pythonDiagnostic struct {
|
|
|
|
Severity pythonSeverity `json:"severity"`
|
|
|
|
Summary string `json:"summary"`
|
|
|
|
Detail string `json:"detail,omitempty"`
|
|
|
|
Location pythonDiagnosticLocation `json:"location,omitempty"`
|
|
|
|
Path string `json:"path,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type pythonDiagnosticLocation struct {
|
|
|
|
File string `json:"file"`
|
|
|
|
Line int `json:"line"`
|
|
|
|
Column int `json:"column"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type pythonSeverity = string
|
|
|
|
|
|
|
|
const (
|
|
|
|
pythonError pythonSeverity = "error"
|
|
|
|
pythonWarning pythonSeverity = "warning"
|
|
|
|
)
|
|
|
|
|
|
|
|
// parsePythonDiagnostics parses diagnostics from the Python mutator.
|
|
|
|
//
|
|
|
|
// diagnostics file is newline-separated JSON objects with pythonDiagnostic structure.
|
|
|
|
func parsePythonDiagnostics(input io.Reader) (diag.Diagnostics, error) {
|
|
|
|
diags := diag.Diagnostics{}
|
|
|
|
decoder := json.NewDecoder(input)
|
|
|
|
|
|
|
|
for decoder.More() {
|
|
|
|
var parsedLine pythonDiagnostic
|
|
|
|
|
|
|
|
err := decoder.Decode(&parsedLine)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse diags: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
severity, err := convertPythonSeverity(parsedLine.Severity)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse severity: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
path, err := convertPythonPath(parsedLine.Path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse path: %s", err)
|
|
|
|
}
|
2024-07-25 15:16:27 +00:00
|
|
|
var paths []dyn.Path
|
|
|
|
if path != nil {
|
|
|
|
paths = []dyn.Path{path}
|
|
|
|
}
|
2024-07-02 15:10:53 +00:00
|
|
|
|
2024-07-23 17:20:11 +00:00
|
|
|
var locations []dyn.Location
|
|
|
|
location := convertPythonLocation(parsedLine.Location)
|
|
|
|
if location != (dyn.Location{}) {
|
|
|
|
locations = append(locations, location)
|
|
|
|
}
|
|
|
|
|
2024-07-02 15:10:53 +00:00
|
|
|
diag := diag.Diagnostic{
|
2024-07-23 17:20:11 +00:00
|
|
|
Severity: severity,
|
|
|
|
Summary: parsedLine.Summary,
|
|
|
|
Detail: parsedLine.Detail,
|
|
|
|
Locations: locations,
|
2024-07-25 15:16:27 +00:00
|
|
|
Paths: paths,
|
2024-07-02 15:10:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
diags = diags.Append(diag)
|
|
|
|
}
|
|
|
|
|
|
|
|
return diags, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func convertPythonPath(path string) (dyn.Path, error) {
|
|
|
|
if path == "" {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return dyn.NewPathFromString(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
func convertPythonSeverity(severity pythonSeverity) (diag.Severity, error) {
|
|
|
|
switch severity {
|
|
|
|
case pythonError:
|
|
|
|
return diag.Error, nil
|
|
|
|
case pythonWarning:
|
|
|
|
return diag.Warning, nil
|
|
|
|
default:
|
|
|
|
return 0, fmt.Errorf("unexpected value: %s", severity)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func convertPythonLocation(location pythonDiagnosticLocation) dyn.Location {
|
|
|
|
return dyn.Location{
|
|
|
|
File: location.File,
|
|
|
|
Line: location.Line,
|
|
|
|
Column: location.Column,
|
|
|
|
}
|
|
|
|
}
|