Skip to content

fix(schema): pathlib.Path is treated as cog.Path; aliased cog types fail - #3171

Open
BarneyChambers wants to merge 5 commits into
replicate:mainfrom
BarneyChambers:fix/pathlib-path-vs-cog-path
Open

fix(schema): pathlib.Path is treated as cog.Path; aliased cog types fail#3171
BarneyChambers wants to merge 5 commits into
replicate:mainfrom
BarneyChambers:fix/pathlib-path-vs-cog-path

Conversation

@BarneyChambers

@BarneyChambers BarneyChambers commented Sep 7, 2026

Copy link
Copy Markdown

Summary

Cog builds an OpenAPI schema from the predictor's type hints at cog build time. File inputs are supposed to be cog.Path. Those show up in the schema as {type: string, format: uri}, and at runtime Cog downloads the URL to a temp file before calling predict().

The schema generator does not check where Path was imported from. Any annotation named Path becomes a file-URI field.

from pathlib import Path
def predict(self, image: Path) -> str: ...     -> schema says URI (wrong; runtime will not download)
from cog import Path as CogPath
def predict(self, image: CogPath) -> str: ...  -> build fails (wrong; this is cog.Path)
from cog import Path
def predict(self, image: Path) -> str: ...     -> schema says URI (correct)

So a normal pathlib import makes the schema lie for inputs, and the obvious workaround (Path as CogPath) does not compile.

File outputs are different. The worker already uploads anything os.PathLike, including pathlib.Path. Those models work today. This change does not fail the build for -> Path.

Real world example

Lots of models already import pathlib, then annotate the file input with Path:

from pathlib import Path
from cog import BasePredictor

class Predictor(BasePredictor):
    def predict(self, image: Path) -> str:
        return image.read_text()

The author wanted a downloaded file. They got stdlib pathlib.Path.

What happens on main today:

  1. Schema generator sees the name Path and emits {type: string, format: uri}.
  2. cog build succeeds.
  3. A client sends an http://... or data:... URL. Schema validation accepts it.
  4. The Rust/Python runtime only downloads URLs for values typed as cog.types.Path. This one is not, so no download.
  5. predict() receives the URL string. image.read_text() raises, or the model treats a URL as a filesystem path.

The image looks fine. The prediction is what breaks.

People then try to keep pathlib and alias the cog type:

from pathlib import Path
from cog import Path as CogPath

def predict(self, image: CogPath) -> str: ...

That fails schema generation too: CogPath is reported as an unknown external type, even though it is cog.Path under an alias.

After this change: an input from pathlib import Path fails the build with a message to use from cog import Path. from cog import Path as CogPath is treated as a real file input (format: uri). A return type of pathlib.Path still builds, same as today.

Fix

  • Look up Path / File / Secret through the import (from cog import Path as CogPath is Path from cog).
  • On inputs only, error if that name came from pathlib (from pathlib import Path, pathlib.Path, import pathlib as p then p.Path).
  • Follow local re-exports: from .types import Path, and import helpers then helpers.Path (including helpers/__init__.py). pathlib if that file imported Path from pathlib, cog.Path if from cog.
  • Leave output pathlib.Path as a file URI. The worker already uploads os.PathLike.
  • A bare Path with no import still counts as cog.Path, same as today. from pathlib import * is the same leftover: the import table never records *. Unresolved relative imports (no file on disk) stay file URIs.

Docs

docs/python.md previously said any os.PathLike subclass was accepted as an input and treated as cog.Path. That does not match coglet, which only downloads cog.types.Path. The docs now require from cog import Path for file inputs, and say a return type of pathlib.Path is still fine.

Not a duplicate

#3094 is about rejecting Input(default=Path("image.png")). Different bug: that one is defaults. This one is the type name resolving to the wrong primitive.

Test

Nothing on main covered pathlib.Path or from cog import Path as CogPath. Existing Path tests all use from cog import Path. Added:

  • TestAliasedCogPathInput
  • TestAliasedCogPathOpenAPIIsURI
  • TestAliasedCogSecretInput
  • TestCogTypesPathImport
  • TestQualifiedCogPathInput
  • TestPathlibPathInputRejected
  • TestPathlibPathOutputAccepted
  • TestRelativeTypesPathInput
  • TestQualifiedPathlibPathRejected
  • TestQualifiedPathlibPathWithoutImportRejected
  • TestPathlibImportedAsAliasRejected
  • TestAliasedCogPathAlongsidePathlib
  • TestRelativePathlibPathInputRejected
  • TestRelativeCogPathReexportAccepted
  • TestImportedModulePathlibPathRejected
  • TestImportedModulePathlibPathAliasedRejected
  • TestImportedModuleCogPathAccepted
  • TestImportedPackagePathlibPathRejected

They fail on main (except the output / unresolved-re-export / cog-reexport cases, which pass on both) and pass with this change.

go test ./pkg/schema/python -count=1 -run 'TestAliasedCogPathInput|TestAliasedCogPathOpenAPIIsURI|TestAliasedCogSecretInput|TestCogTypesPathImport|TestQualifiedCogPathInput|TestPathlibPathInputRejected|TestPathlibPathOutputAccepted|TestRelativeTypesPathInput|TestQualifiedPathlibPathRejected|TestQualifiedPathlibPathWithoutImportRejected|TestPathlibImportedAsAliasRejected|TestAliasedCogPathAlongsidePathlib|TestRelativePathlibPathInputRejected|TestRelativeCogPathReexportAccepted|TestListPathInput|TestSecretType|TestFileType' -v

This changes primitive resolution in pkg/schema and follows Path/File/Secret through local modules after they are loaded. The runtime already checks cog.types.Path by identity for downloads, which is correct. The schema was the part that was wrong on inputs.

Verification

  • Run the tests above.
  • from cog import Path as CogPath produces format: uri.
  • Input from pathlib import Path fails the build.
  • Input pathlib.Path fails the build even without import pathlib.
  • from .types import Path and import helpers / helpers.Path fail when the local module re-exports pathlib, and succeed when it re-exports cog.Path.
  • Return type pathlib.Path still builds.
  • from cog import Path and cog.Path still work.
  • No config change.
  • Docs: file inputs must be cog.Path; pathlib.Path is still valid as a return type.

PrimitiveFromName matched the local identifier Path, so pathlib.Path
got format:uri in the OpenAPI schema while the runtime never downloaded
the file. from cog import Path as CogPath failed for the same reason.

Signed-off-by: barneychambers <barneychambers@hotmail.com>
The same message fires for return annotations, so calling them inputs
was wrong.

Signed-off-by: barneychambers <barneychambers@hotmail.com>
Outputs already upload os.PathLike, so failing the build for
`-> pathlib.Path` was a regression. Relative re-exports like
`from .types import Path` are left as file URIs.

Signed-off-by: barneychambers <barneychambers@hotmail.com>
@BarneyChambers
BarneyChambers requested a review from a team as a code owner September 7, 2026 11:03
Docs still said PathLike inputs were cog.Path. They are not: coglet
only downloads cog.types.Path. Say that, reject pathlib.Path even
without an import, and follow local `from .types import Path` when
the file re-exports pathlib.

Signed-off-by: barneychambers <barneychambers@hotmail.com>
from .types import Path was followed, but import helpers then
helpers.Path still emitted a file URI. Record Path/File/Secret on
local module imports, including helpers/__init__.py packages.

Signed-off-by: barneychambers <barneychambers@hotmail.com>
@BarneyChambers

BarneyChambers commented Sep 7, 2026

Copy link
Copy Markdown
Author

@replicate/cog this is ready for review. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant