Skip to content

Commit

Permalink
Merge pull request #970 from posit-dev/mm-shiny-express
Browse files Browse the repository at this point in the history
add Shiny express detection
  • Loading branch information
mmarchetti authored Feb 13, 2024
2 parents 4d0a3d5 + a9c55e2 commit 3c741ff
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 6 deletions.
81 changes: 81 additions & 0 deletions internal/inspect/detectors/pyshiny.go
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
}
66 changes: 66 additions & 0 deletions internal/inspect/detectors/pyshiny_test.go
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)
}
6 changes: 0 additions & 6 deletions internal/inspect/detectors/python.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@ func NewBokehDetector() *PythonAppDetector {
})
}

func NewPyShinyDetector() *PythonAppDetector {
return NewPythonAppDetector(config.ContentTypePythonShiny, []string{
"shiny",
})
}

func (d *PythonAppDetector) InferType(path util.Path) (*config.Config, error) {
entrypoint, entrypointPath, err := d.InferEntrypoint(
path, ".py", "main.py", "app.py", "streamlit_app.py", "api.py")
Expand Down

0 comments on commit 3c741ff

Please sign in to comment.