-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #970 from posit-dev/mm-shiny-express
add Shiny express detection
- Loading branch information
Showing
3 changed files
with
147 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package detectors | ||
|
||
// Copyright (C) 2023 by Posit Software, PBC. | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/rstudio/connect-client/internal/config" | ||
"github.com/rstudio/connect-client/internal/util" | ||
) | ||
|
||
type pyShinyDetector struct { | ||
inferenceHelper | ||
} | ||
|
||
func NewPyShinyDetector() *pyShinyDetector { | ||
return &pyShinyDetector{ | ||
inferenceHelper: defaultInferenceHelper{}, | ||
} | ||
} | ||
|
||
var shinyExpressImportRE = regexp.MustCompile(`(import\s+shiny.express)|(from\s+shiny.express\s+import)|(from\s+shiny\s+import.*\bexpress\b)`) | ||
|
||
func hasShinyExpressImport(content string) bool { | ||
return shinyExpressImportRE.MatchString(content) | ||
} | ||
|
||
func fileHasShinyExpressImport(path util.Path) (bool, error) { | ||
content, err := path.ReadFile() | ||
if err != nil { | ||
return false, err | ||
} | ||
return hasShinyExpressImport(string(content)), nil | ||
} | ||
|
||
var invalidPythonIdentifierRE = regexp.MustCompile(`(^[0-9]|[^A-Za-z0-9])`) | ||
|
||
func shinyExpressEntrypoint(entrypoint string) string { | ||
module := strings.TrimSuffix(entrypoint, ".py") | ||
|
||
safeEntrypoint := invalidPythonIdentifierRE.ReplaceAllStringFunc(module, func(match string) string { | ||
return fmt.Sprintf("_%x_", int(match[0])) | ||
}) | ||
return "shiny.express.app:" + safeEntrypoint | ||
} | ||
|
||
func (d *pyShinyDetector) InferType(path util.Path) (*config.Config, error) { | ||
entrypoint, entrypointPath, err := d.InferEntrypoint(path, ".py", "main.py", "app.py") | ||
if err != nil { | ||
return nil, err | ||
} | ||
if entrypoint == "" { | ||
// We didn't find a matching filename | ||
return nil, nil | ||
} | ||
matches, err := d.FileHasPythonImports(entrypointPath, []string{"shiny"}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if !matches { | ||
// Not a PyShiny app | ||
return nil, nil | ||
} | ||
isShinyExpress, err := fileHasShinyExpressImport(entrypointPath) | ||
if err != nil { | ||
return nil, err | ||
} | ||
cfg := config.New() | ||
|
||
if isShinyExpress { | ||
cfg.Entrypoint = shinyExpressEntrypoint(entrypoint) | ||
} else { | ||
cfg.Entrypoint = entrypoint | ||
} | ||
cfg.Type = config.ContentTypePythonShiny | ||
// indicate that Python inspection is needed | ||
cfg.Python = &config.Python{} | ||
return cfg, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package detectors | ||
|
||
// Copyright (C) 2023 by Posit Software, PBC. | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/rstudio/connect-client/internal/config" | ||
"github.com/rstudio/connect-client/internal/schema" | ||
"github.com/rstudio/connect-client/internal/util" | ||
"github.com/rstudio/connect-client/internal/util/utiltest" | ||
"github.com/spf13/afero" | ||
"github.com/stretchr/testify/suite" | ||
) | ||
|
||
type PyShinySuite struct { | ||
utiltest.Suite | ||
} | ||
|
||
func TestPyShinySuite(t *testing.T) { | ||
suite.Run(t, new(PyShinySuite)) | ||
} | ||
|
||
func (s *PyShinySuite) TestInferType() { | ||
base := util.NewPath("/project", afero.NewMemMapFs()) | ||
err := base.MkdirAll(0777) | ||
s.NoError(err) | ||
|
||
filename := "app.py" | ||
path := base.Join(filename) | ||
err = path.WriteFile([]byte("import shiny\n"), 0600) | ||
s.Nil(err) | ||
|
||
detector := NewPyShinyDetector() | ||
t, err := detector.InferType(base) | ||
s.Nil(err) | ||
s.Equal(&config.Config{ | ||
Schema: schema.ConfigSchemaURL, | ||
Type: config.ContentTypePythonShiny, | ||
Entrypoint: filename, | ||
Validate: true, | ||
Python: &config.Python{}, | ||
}, t) | ||
} | ||
|
||
func (s *PyShinySuite) TestInferTypeShinyExpress() { | ||
base := util.NewPath("/project", afero.NewMemMapFs()) | ||
err := base.MkdirAll(0777) | ||
s.NoError(err) | ||
|
||
filename := "app.py" | ||
path := base.Join(filename) | ||
err = path.WriteFile([]byte("import shiny.express\n"), 0600) | ||
s.Nil(err) | ||
|
||
detector := NewPyShinyDetector() | ||
t, err := detector.InferType(base) | ||
s.Nil(err) | ||
s.Equal(&config.Config{ | ||
Schema: schema.ConfigSchemaURL, | ||
Type: config.ContentTypePythonShiny, | ||
Entrypoint: "shiny.express.app:app", | ||
Validate: true, | ||
Python: &config.Python{}, | ||
}, t) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters