diff --git a/docs/llms.txt b/docs/llms.txt index 3980e3ef3d..9e725e7096 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -3247,7 +3247,9 @@ These types can be used directly as input parameter types and output return type `cog.Path` is used to get files in and out of models. It represents a _path to a file on disk_. -`cog.Path` is a subclass of Python's [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#basic-use) and can be used as a drop-in replacement. Any `os.PathLike` subclass is also accepted as an input type and treated as `cog.Path`. +File inputs must be `cog.Path` (`from cog import Path`). Clients send a URL; Cog downloads it to a temp file before `run()`. `from pathlib import Path` on an input fails `cog build`, because that download only happens for `cog.Path`. + +`cog.Path` is a subclass of Python's [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#basic-use). A return type of `pathlib.Path` still works: Cog uploads any `os.PathLike`. For models that return a `cog.Path` object, the output returned by Cog's built-in HTTP server will be a URL. diff --git a/docs/python.md b/docs/python.md index 069a17abe8..ecf6320b2e 100644 --- a/docs/python.md +++ b/docs/python.md @@ -544,7 +544,9 @@ These types can be used directly as input parameter types and output return type `cog.Path` is used to get files in and out of models. It represents a _path to a file on disk_. -`cog.Path` is a subclass of Python's [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#basic-use) and can be used as a drop-in replacement. Any `os.PathLike` subclass is also accepted as an input type and treated as `cog.Path`. +File inputs must be `cog.Path` (`from cog import Path`). Clients send a URL; Cog downloads it to a temp file before `run()`. `from pathlib import Path` on an input fails `cog build`, because that download only happens for `cog.Path`. + +`cog.Path` is a subclass of Python's [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#basic-use). A return type of `pathlib.Path` still works: Cog uploads any `os.PathLike`. For models that return a `cog.Path` object, the output returned by Cog's built-in HTTP server will be a URL. diff --git a/pkg/schema/errors.go b/pkg/schema/errors.go index 4cd07149e4..8131a564ae 100644 --- a/pkg/schema/errors.go +++ b/pkg/schema/errors.go @@ -1,6 +1,9 @@ package schema -import "fmt" +import ( + "fmt" + "strings" +) // SchemaError represents errors during schema generation. type SchemaError struct { @@ -112,6 +115,19 @@ func errUnresolvableImportedType(name, module string) error { } } +func errNotCogFileLike(localName, module, original string) error { + msg := fmt.Sprintf( + "%s is %s.%s, not cog.%s. Use `from cog import %s` for file inputs", + localName, module, original, original, original) + if !strings.Contains(localName, ".") { + msg += fmt.Sprintf(". If you also need %s.%s, import it under a different name", module, original) + } + return &SchemaError{ + Kind: ErrUnsupportedType, + Message: msg, + } +} + func errUnresolvableType(name string) error { return &SchemaError{ Kind: ErrUnresolvableType, diff --git a/pkg/schema/python/models.go b/pkg/schema/python/models.go index 94e9ed1a40..7490ba124f 100644 --- a/pkg/schema/python/models.go +++ b/pkg/schema/python/models.go @@ -144,6 +144,7 @@ func (ctx *modelParseContext) loadModelsFromModule(sourceDir, module string) sch if pyPath == "" { return nil } + pyPath = existingPythonFile(sourceDir, pyPath) cacheKey := filepath.Clean(pyPath) if summary, ok := ctx.loadedModules[cacheKey]; ok { return summary.Models @@ -260,6 +261,110 @@ func nestedImportModule(module string, original string) string { return module + "." + original } +// followReexportedFileLikes rewrites Path/File/Secret imports that came from a +// local module so they point at the original cog or pathlib binding. +// +// # types.py +// from pathlib import Path +// # predict.py +// from .types import Path +// +// becomes pathlib.Path, which inputs then reject. Unresolved relative imports +// (no file on disk) are left as file URIs, same as a missing local BaseModel. +func followReexportedFileLikes(imports *schema.ImportContext, loaded map[string]ModuleSummary, sourcePath string) { + if imports == nil || loaded == nil || len(loaded) == 0 { + return + } + type rewrite struct { + local string + entry schema.ImportEntry + } + var rewrites []rewrite + imports.Names.Entries(func(localName string, entry schema.ImportEntry) { + resolved := followFileLikeOrigin(entry, loaded, sourcePath, 0) + if resolved == entry { + return + } + if resolved.Original != "Path" && resolved.Original != "File" && resolved.Original != "Secret" { + return + } + if resolved.Module != "cog" && !strings.HasPrefix(resolved.Module, "cog.") && + resolved.Module != "pathlib" && !strings.HasPrefix(resolved.Module, "pathlib.") { + return + } + rewrites = append(rewrites, rewrite{local: localName, entry: resolved}) + }) + for _, r := range rewrites { + imports.Names.Set(r.local, r.entry) + } + recordImportedModuleFileLikes(imports, loaded, sourcePath) +} + +// recordImportedModuleFileLikes records Path/File/Secret on `import helpers` +// so `helpers.Path` follows the same pathlib/cog origin as `from helpers import Path`. +func recordImportedModuleFileLikes(imports *schema.ImportContext, loaded map[string]ModuleSummary, sourcePath string) { + if imports.ModuleAttrs == nil { + imports.ModuleAttrs = map[string]map[string]schema.ImportEntry{} + } + imports.Names.Entries(func(localName string, entry schema.ImportEntry) { + // parseImport stores Original == Module for `import foo` and `import foo as bar`. + if entry.Original != entry.Module { + return + } + if isKnownExternalModule(entry.Module) { + return + } + pyPath := moduleToFilePath(entry.Module, sourcePath) + summary, ok := lookupLoaded(loaded, pyPath) + if !ok || summary.Imports == nil { + return + } + attrs := map[string]schema.ImportEntry{} + summary.Imports.Names.Entries(func(attr string, inner schema.ImportEntry) { + resolved := followFileLikeOrigin(inner, loaded, summary.SourcePath, 0) + if resolved.Original != "Path" && resolved.Original != "File" && resolved.Original != "Secret" { + return + } + if resolved.Module != "cog" && !strings.HasPrefix(resolved.Module, "cog.") && + resolved.Module != "pathlib" && !strings.HasPrefix(resolved.Module, "pathlib.") { + return + } + attrs[attr] = resolved + }) + if len(attrs) > 0 { + imports.ModuleAttrs[localName] = attrs + } + }) +} + +func followFileLikeOrigin(entry schema.ImportEntry, loaded map[string]ModuleSummary, sourcePath string, depth int) schema.ImportEntry { + if depth > 8 { + return entry + } + if entry.Module == "cog" || strings.HasPrefix(entry.Module, "cog.") { + return entry + } + if entry.Module == "pathlib" || strings.HasPrefix(entry.Module, "pathlib.") { + return entry + } + if isKnownExternalModule(entry.Module) { + return entry + } + pyPath := moduleToFilePath(entry.Module, sourcePath) + if pyPath == "" { + return entry + } + summary, ok := lookupLoaded(loaded, pyPath) + if !ok || summary.Imports == nil { + return entry + } + next, ok := summary.Imports.Names.Get(entry.Original) + if !ok { + return entry + } + return followFileLikeOrigin(next, loaded, summary.SourcePath, depth+1) +} + func refreshLoadedModuleAliases(loadedModules map[string]ModuleSummary) { for _, summary := range loadedModules { if summary.Imports == nil || summary.Models == nil { @@ -279,6 +384,32 @@ func refreshLoadedModuleAliases(loadedModules map[string]ModuleSummary) { } } +func lookupLoaded(loaded map[string]ModuleSummary, pyPath string) (ModuleSummary, bool) { + if pyPath == "" { + return ModuleSummary{}, false + } + if summary, ok := loaded[filepath.Clean(pyPath)]; ok { + return summary, true + } + initPath := filepath.Join(strings.TrimSuffix(pyPath, ".py"), "__init__.py") + summary, ok := loaded[filepath.Clean(initPath)] + return summary, ok +} + +func existingPythonFile(sourceDir, pyPath string) string { + if sourceDir == "" || pyPath == "" { + return pyPath + } + if _, err := os.Stat(filepath.Join(sourceDir, pyPath)); err == nil { + return pyPath + } + initRel := filepath.Join(strings.TrimSuffix(pyPath, ".py"), "__init__.py") + if _, err := os.Stat(filepath.Join(sourceDir, initRel)); err == nil { + return initRel + } + return pyPath +} + // moduleToFilePath converts a Python module path to a relative .py file path. // // ".types", "pkg/predict.py" → "pkg/types.py" diff --git a/pkg/schema/python/parser.go b/pkg/schema/python/parser.go index 093a1e6e85..f4263b9e66 100644 --- a/pkg/schema/python/parser.go +++ b/pkg/schema/python/parser.go @@ -156,6 +156,7 @@ func resolveImportedModelsPhase(state *ParseState) error { state.ModelCtx.resolvedModels = state.Models setDiscoveredModels(state.Models, collectModelClasses(state.Root, state.Options.Source, state.ModelCtx)) } + followReexportedFileLikes(state.Imports, state.LoadedModules, state.Options.SourcePath) return nil } diff --git a/pkg/schema/python/parser_test.go b/pkg/schema/python/parser_test.go index c6140b30a6..f889d15d63 100644 --- a/pkg/schema/python/parser_test.go +++ b/pkg/schema/python/parser_test.go @@ -2234,6 +2234,321 @@ class Predictor(BasePredictor): require.Equal(t, schema.Repeated, files.FieldType.Repetition) } +func TestAliasedCogPathInput(t *testing.T) { + source := ` +from cog import BasePredictor, Path as CogPath + +class Predictor(BasePredictor): + def predict(self, image: CogPath) -> str: + pass +` + info := parse(t, source, "Predictor") + image, ok := info.Inputs.Get("image") + require.True(t, ok) + require.Equal(t, schema.TypePath, image.FieldType.Primitive) +} + +func TestAliasedCogPathOpenAPIIsURI(t *testing.T) { + source := []byte(` +from cog import BasePredictor, Path as CogPath + +class Predictor(BasePredictor): + def predict(self, image: CogPath) -> str: + return "ok" +`) + info, err := ParsePredictor(source, "Predictor", schema.ModePredict, "") + require.NoError(t, err) + + out, err := schema.GenerateOpenAPISchema(info) + require.NoError(t, err) + + var doc map[string]any + require.NoError(t, json.Unmarshal(out, &doc)) + input := doc["components"].(map[string]any)["schemas"].(map[string]any)["Input"].(map[string]any) + prop := input["properties"].(map[string]any)["image"].(map[string]any) + require.Equal(t, "string", prop["type"]) + require.Equal(t, "uri", prop["format"]) +} + +func TestAliasedCogSecretInput(t *testing.T) { + source := ` +from cog import BasePredictor, Secret as Token + +class Predictor(BasePredictor): + def predict(self, api_key: Token) -> str: + pass +` + info := parse(t, source, "Predictor") + apiKey, ok := info.Inputs.Get("api_key") + require.True(t, ok) + require.Equal(t, schema.TypeSecret, apiKey.FieldType.Primitive) +} + +func TestCogTypesPathImport(t *testing.T) { + source := ` +from cog import BasePredictor +from cog.types import Path + +class Predictor(BasePredictor): + def predict(self, image: Path) -> str: + pass +` + info := parse(t, source, "Predictor") + image, ok := info.Inputs.Get("image") + require.True(t, ok) + require.Equal(t, schema.TypePath, image.FieldType.Primitive) +} + +func TestQualifiedCogPathInput(t *testing.T) { + source := ` +import cog + +class Predictor(cog.BasePredictor): + def predict(self, image: cog.Path) -> str: + pass +` + info := parse(t, source, "Predictor") + image, ok := info.Inputs.Get("image") + require.True(t, ok) + require.Equal(t, schema.TypePath, image.FieldType.Primitive) +} + +func TestPathlibPathInputRejected(t *testing.T) { + source := ` +from pathlib import Path +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: Path) -> str: + pass +` + se := parseErr(t, source, "Predictor", schema.ModePredict) + require.Equal(t, schema.ErrUnsupportedType, se.Kind) + require.Contains(t, se.Error(), "pathlib") + require.Contains(t, se.Error(), "from cog import Path") +} + +func TestPathlibPathOutputAccepted(t *testing.T) { + source := ` +from pathlib import Path +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, prompt: str) -> Path: + pass +` + info := parse(t, source, "Predictor") + require.Equal(t, schema.SchemaPrimitive, info.Output.Kind) + require.Equal(t, schema.TypePath, info.Output.Primitive) +} + +func TestRelativeTypesPathInput(t *testing.T) { + // No types.py on disk, so the re-export cannot be followed. Same as today. + source := ` +from .types import Path +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: Path) -> str: + pass +` + info := parse(t, source, "Predictor") + image, ok := info.Inputs.Get("image") + require.True(t, ok) + require.Equal(t, schema.TypePath, image.FieldType.Primitive) +} + +func TestQualifiedPathlibPathRejected(t *testing.T) { + source := ` +import pathlib +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: pathlib.Path) -> str: + pass +` + se := parseErr(t, source, "Predictor", schema.ModePredict) + require.Equal(t, schema.ErrUnsupportedType, se.Kind) + require.Contains(t, se.Error(), "pathlib") + require.Contains(t, se.Error(), "from cog import Path") + require.NotContains(t, se.Error(), "different name") +} + +func TestQualifiedPathlibPathWithoutImportRejected(t *testing.T) { + source := ` +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: pathlib.Path) -> str: + pass +` + se := parseErr(t, source, "Predictor", schema.ModePredict) + require.Equal(t, schema.ErrUnsupportedType, se.Kind) + require.Contains(t, se.Error(), "pathlib") + require.NotContains(t, se.Error(), "different name") +} + +func TestPathlibImportedAsAliasRejected(t *testing.T) { + source := ` +import pathlib as p +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: p.Path) -> str: + pass +` + se := parseErr(t, source, "Predictor", schema.ModePredict) + require.Equal(t, schema.ErrUnsupportedType, se.Kind) + require.Contains(t, se.Error(), "pathlib") +} + +func TestAliasedCogPathAlongsidePathlib(t *testing.T) { + source := ` +from pathlib import Path +from cog import BasePredictor, Path as CogPath + +class Predictor(BasePredictor): + def predict(self, image: CogPath) -> str: + dest = Path("/tmp/out.txt") + dest.write_text("ok") + return dest.read_text() +` + info := parse(t, source, "Predictor") + image, ok := info.Inputs.Get("image") + require.True(t, ok) + require.Equal(t, schema.TypePath, image.FieldType.Primitive) +} + +func TestRelativePathlibPathInputRejected(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "types.py", ` +from pathlib import Path +`) + writeFile(t, dir, "predict.py", ` +from .types import Path +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: Path) -> str: + pass +`) + source, err := os.ReadFile(filepath.Join(dir, "predict.py")) + require.NoError(t, err) + _, parseErr := ParsePredictorWithSourcePath(source, "Predictor", schema.ModePredict, dir, "predict.py") + require.Error(t, parseErr) + var se *schema.SchemaError + require.True(t, errors.As(parseErr, &se), "expected *schema.SchemaError, got %T: %v", parseErr, parseErr) + require.Equal(t, schema.ErrUnsupportedType, se.Kind) + require.Contains(t, se.Error(), "pathlib") +} + +func TestRelativeCogPathReexportAccepted(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "types.py", ` +from cog import Path +`) + writeFile(t, dir, "predict.py", ` +from .types import Path +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: Path) -> str: + pass +`) + info := parseFile(t, dir, "predict.py", "Predictor") + image, ok := info.Inputs.Get("image") + require.True(t, ok) + require.Equal(t, schema.TypePath, image.FieldType.Primitive) +} + +func TestImportedModulePathlibPathRejected(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "helpers.py", ` +from pathlib import Path +`) + writeFile(t, dir, "predict.py", ` +import helpers +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: helpers.Path) -> str: + pass +`) + source, err := os.ReadFile(filepath.Join(dir, "predict.py")) + require.NoError(t, err) + _, parseErr := ParsePredictorWithSourcePath(source, "Predictor", schema.ModePredict, dir, "predict.py") + require.Error(t, parseErr) + var se *schema.SchemaError + require.True(t, errors.As(parseErr, &se), "expected *schema.SchemaError, got %T: %v", parseErr, parseErr) + require.Equal(t, schema.ErrUnsupportedType, se.Kind) + require.Contains(t, se.Error(), "pathlib") +} + +func TestImportedModulePathlibPathAliasedRejected(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "helpers.py", ` +from pathlib import Path +`) + writeFile(t, dir, "predict.py", ` +import helpers as h +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: h.Path) -> str: + pass +`) + source, err := os.ReadFile(filepath.Join(dir, "predict.py")) + require.NoError(t, err) + _, parseErr := ParsePredictorWithSourcePath(source, "Predictor", schema.ModePredict, dir, "predict.py") + require.Error(t, parseErr) + var se *schema.SchemaError + require.True(t, errors.As(parseErr, &se), "expected *schema.SchemaError, got %T: %v", parseErr, parseErr) + require.Equal(t, schema.ErrUnsupportedType, se.Kind) + require.Contains(t, se.Error(), "pathlib") +} + +func TestImportedModuleCogPathAccepted(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "helpers.py", ` +from cog import Path +`) + writeFile(t, dir, "predict.py", ` +import helpers +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: helpers.Path) -> str: + pass +`) + info := parseFile(t, dir, "predict.py", "Predictor") + image, ok := info.Inputs.Get("image") + require.True(t, ok) + require.Equal(t, schema.TypePath, image.FieldType.Primitive) +} + +func TestImportedPackagePathlibPathRejected(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "helpers/__init__.py", ` +from pathlib import Path +`) + writeFile(t, dir, "predict.py", ` +import helpers +from cog import BasePredictor + +class Predictor(BasePredictor): + def predict(self, image: helpers.Path) -> str: + pass +`) + source, err := os.ReadFile(filepath.Join(dir, "predict.py")) + require.NoError(t, err) + _, parseErr := ParsePredictorWithSourcePath(source, "Predictor", schema.ModePredict, dir, "predict.py") + require.Error(t, parseErr) + var se *schema.SchemaError + require.True(t, errors.As(parseErr, &se), "expected *schema.SchemaError, got %T: %v", parseErr, parseErr) + require.Equal(t, schema.ErrUnsupportedType, se.Kind) + require.Contains(t, se.Error(), "pathlib") +} + // --------------------------------------------------------------------------- // Optional list inputs (list[X] | None) // --------------------------------------------------------------------------- diff --git a/pkg/schema/schema_type.go b/pkg/schema/schema_type.go index 986eac7ddd..6859cd1141 100644 --- a/pkg/schema/schema_type.go +++ b/pkg/schema/schema_type.go @@ -311,7 +311,10 @@ func resolveSimpleSchemaType(ann TypeAnnotation, ctx *ImportContext, models Mode return SchemaArrayOf(SchemaAnyType()), nil } - prim, ok := PrimitiveFromName(name) + prim, ok, err := resolvePrimitiveType(ann.Name, ctx, false) + if err != nil { + return SchemaType{}, err + } if !ok { if qualified && qualifiedEntry.Module != "" { return SchemaType{}, errUnresolvableImportedType(name, qualifiedEntry.Module) @@ -320,6 +323,9 @@ func resolveSimpleSchemaType(ann TypeAnnotation, ctx *ImportContext, models Mode if qualifiedEntry.Module != "" { return SchemaType{}, errUnresolvableImportedType(name, qualifiedEntry.Module) } + if entry, imported := ctx.Names.Get(ann.Name); imported { + return SchemaType{}, errUnresolvableImportedType(ann.Name, entry.Module) + } if entry, imported := ctx.Names.Get(name); imported { return SchemaType{}, errUnresolvableImportedType(name, entry.Module) } diff --git a/pkg/schema/types.go b/pkg/schema/types.go index 5950b136eb..5cb1c8e3cc 100644 --- a/pkg/schema/types.go +++ b/pkg/schema/types.go @@ -82,6 +82,66 @@ func PrimitiveFromName(name string) (PrimitiveType, bool) { } } +func isCogFileLikePrimitive(name string) bool { + return name == "Path" || name == "File" || name == "Secret" +} + +func isPathlibModule(module string) bool { + return module == "pathlib" || strings.HasPrefix(module, "pathlib.") +} + +// resolvePrimitiveType maps an annotation name to a PrimitiveType. +// +// Path/File/Secret are cog types. The schema generator used to call +// PrimitiveFromName on the local identifier, so: +// +// from pathlib import Path -> TypePath (wrong on inputs; runtime will not download) +// from cog import Path as CogPath -> unresolvable (wrong; this is cog.Path) +// +// Resolve via the import's original name. On inputs, reject pathlib.Path +// (from pathlib import Path, pathlib.Path, from .types import Path that +// re-exports pathlib, and import helpers then helpers.Path). Outputs keep +// treating pathlib.Path as a file URI because the worker already uploads +// os.PathLike. +func resolvePrimitiveType(annName string, ctx *ImportContext, rejectPathlib bool) (PrimitiveType, bool, error) { + lookupName := annName + entry := ImportEntry{} + imported := false + + if resolved, e, ok := ctx.ResolveQualifiedName(annName); ok { + lookupName = resolved + if e.Module != "" { + entry = e + imported = true + } + } else if e, ok := ctx.Names.Get(annName); ok { + lookupName = e.Original + entry = e + imported = true + } + + prim, ok := PrimitiveFromName(lookupName) + if !ok { + return 0, false, nil + } + if rejectPathlib && isCogFileLikePrimitive(lookupName) && isPathlibAnnotation(annName, entry, imported) { + module := entry.Module + if module == "" { + module = "pathlib" + } + return 0, false, errNotCogFileLike(annName, module, lookupName) + } + return prim, true, nil +} + +func isPathlibAnnotation(annName string, entry ImportEntry, imported bool) bool { + if imported && isPathlibModule(entry.Module) { + return true + } + // ResolveQualifiedName("pathlib.Path") with no import still unwraps to Path. + return annName == "pathlib.Path" || strings.HasPrefix(annName, "pathlib.") +} + // Repetition describes cardinality of a field. type Repetition int @@ -279,6 +339,10 @@ const ( type ImportContext struct { // Names maps local name → (module, original_name) Names *OrderedMap[string, ImportEntry] + // ModuleAttrs maps `import helpers` / `import helpers as h` to Path/File/Secret + // names found in that local module, after following re-exports. + // `h.Path` looks up ModuleAttrs["h"]["Path"]. + ModuleAttrs map[string]map[string]ImportEntry } // ImportEntry records where a name was imported from. @@ -289,7 +353,10 @@ type ImportEntry struct { // NewImportContext creates an empty ImportContext. func NewImportContext() *ImportContext { - return &ImportContext{Names: NewOrderedMap[string, ImportEntry]()} + return &ImportContext{ + Names: NewOrderedMap[string, ImportEntry](), + ModuleAttrs: map[string]map[string]ImportEntry{}, + } } // IsCogType returns true if name was imported from the "cog" module. @@ -372,9 +439,25 @@ func (ctx *ImportContext) ResolveQualifiedName(name string) (string, ImportEntry if !ok { return parts[1], ImportEntry{}, true } + if inner, ok := ctx.ModuleAttr(parts[0], parts[1]); ok { + return inner.Original, inner, true + } return parts[1], entry, true } +// ModuleAttr returns a name exported by a locally imported module, if recorded. +func (ctx *ImportContext) ModuleAttr(alias, attr string) (ImportEntry, bool) { + if ctx == nil || ctx.ModuleAttrs == nil { + return ImportEntry{}, false + } + attrs, ok := ctx.ModuleAttrs[alias] + if !ok { + return ImportEntry{}, false + } + entry, ok := attrs[attr] + return entry, ok +} + // ResolveFieldType resolves a TypeAnnotation into a FieldType. func ResolveFieldType(ann TypeAnnotation, ctx *ImportContext, typedDicts map[string]bool) (FieldType, error) { if inner, ok := unwrapOpaqueAnnotated(ann, ctx); ok { @@ -402,11 +485,17 @@ func ResolveFieldType(ann TypeAnnotation, ctx *ImportContext, typedDicts map[str if name == "dict" || name == "Dict" { return FieldType{Primitive: TypeAny, Repetition: Required}, nil } - prim, ok := PrimitiveFromName(name) + prim, ok, err := resolvePrimitiveType(ann.Name, ctx, true) + if err != nil { + return FieldType{}, err + } if !ok { if qualifiedEntry.Module != "" { return FieldType{}, errUnresolvableImportedType(name, qualifiedEntry.Module) } + if entry, imported := ctx.Names.Get(ann.Name); imported { + return FieldType{}, errUnresolvableImportedType(ann.Name, entry.Module) + } if entry, imported := ctx.Names.Get(name); imported { return FieldType{}, errUnresolvableImportedType(name, entry.Module) } @@ -525,11 +614,17 @@ func resolveInputType(ann TypeAnnotation, ctx *ImportContext, typedDicts map[str if name == "dict" || name == "Dict" { return InputAnyType(), nil } - prim, ok := PrimitiveFromName(name) + prim, ok, err := resolvePrimitiveType(ann.Name, ctx, true) + if err != nil { + return InputType{}, err + } if !ok { if qualifiedEntry.Module != "" { return InputType{}, errUnresolvableImportedType(name, qualifiedEntry.Module) } + if entry, imported := ctx.Names.Get(ann.Name); imported { + return InputType{}, errUnresolvableImportedType(ann.Name, entry.Module) + } if entry, imported := ctx.Names.Get(name); imported { return InputType{}, errUnresolvableImportedType(name, entry.Module) } diff --git a/python/cog/_adt.py b/python/cog/_adt.py index 3fea80cc27..556a5a33bb 100644 --- a/python/cog/_adt.py +++ b/python/cog/_adt.py @@ -153,6 +153,10 @@ def from_type(tpe: type | Any) -> "PrimitiveType": return match try: + # cog.Path matches identity above. PathLike stays PATH so + # pathlib.Path *return* values still encode as file URIs. + # File inputs must be cog.Path; cog build rejects pathlib.Path + # on inputs in the Go schema generator. if tpe is os.PathLike or ( isinstance(tpe, type) and issubclass(tpe, os.PathLike) # type: ignore[arg-type] ):