diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e022641e2c..78a78bb8c2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,8 +35,7 @@ jobs: with: python-version: 3.11 - name: Install Python dependencies - run: | - python -m pip install '.[dev]' + run: make install-python - name: Build run: make cog - name: Lint @@ -60,8 +59,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install Python dependencies - run: | - python -m pip install '.[dev]' + run: make install-python - name: Test run: make test-python env: @@ -79,11 +77,9 @@ jobs: with: python-version: "3.11" - name: Install Python dependencies - run: | - python -m pip install '.[dev]' + run: make install-python - name: Run typechecking - run: | - make lint-python + run: make lint-python # cannot run this on mac due to licensing issues: https://github.com/actions/virtual-environments/issues/2150 test-integration: @@ -98,8 +94,7 @@ jobs: with: python-version: 3.11 - name: Install Python dependencies - run: | - python -m pip install '.[dev]' + run: make install-python - name: Test run: make test-integration diff --git a/.gitignore b/.gitignore index 4cde3813fc..998867f4ba 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ pkg/dockerfile/embed/cog.whl .DS_Store docs/README.md docs/CONTRIBUTING.md +python/cog/_vendor/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39db059526..9a8dc38f39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -207,6 +207,23 @@ To publish a prerelease version, append a [SemVer prerelease identifer](https:// git tag -a v0.1.0-alpha -m "Prerelease v0.1.0" git push --tags +## Vendoring Python packages + +We vendor the few Python libraries we depend on to avoid dependency hell. We use [vendoring](https://pypi.org/project/vendoring/), the same tool used by pip. + +The vendored packages are defined in `python/cog/_vendor/vendor.txt`. If you add/change anything in there, the default `make` rule will sync all vendored packages. + +If you run into issues you may need to manually add a patch. Patches are stored in `python/tools/vendoring/patches`. + +To create a new patch, copy the file you want to patch to `$FILENAME.new`, and edit that file as needed. For example: + + cp python/cog/_vendor/curio/__main__.py python/cog/_vendor/curio/__main__.py.new + +Then `diff` the new and old version to a patch file in `python/tools/vendoring/patches/$PACKAGE.patch` and re-run vendoring. For example: + + diff -u ./python/cog/_vendor/curio/__main__.py ./python/cog/_vendor/curio/__main__.py.new >> python/tools/vendoring/patches/curio.patch + make + ## Troubleshooting ### `cog command not found` diff --git a/Makefile b/Makefile index 6e2a6a9018..a91cc83661 100644 --- a/Makefile +++ b/Makefile @@ -15,16 +15,21 @@ PYTHON ?= python PYTEST := $(PYTHON) -m pytest PYRIGHT := $(PYTHON) -m pyright RUFF := $(PYTHON) -m ruff +PIP := $(PYTHON) -m pip +VENDORING := $(PYTHON) -m vendoring default: all .PHONY: all all: cog -pkg/dockerfile/embed/cog.whl: python/* python/cog/* python/cog/server/* python/cog/command/* +python/cog/_vendor: python/cog/vendor.txt + $(VENDORING) sync + +pkg/dockerfile/embed/cog.whl: python/* python/cog/* python/cog/server/* python/cog/command/* python/cog/_vendor @echo "Building Python library" rm -rf dist - $(PYTHON) -m pip install build && $(PYTHON) -m build --wheel + $(PIP) install build && $(PYTHON) -m build --wheel mkdir -p pkg/dockerfile/embed cp dist/*.whl $@ @@ -68,7 +73,7 @@ test-integration: cog cd test-integration/ && $(MAKE) PATH="$(PWD):$(PATH)" test .PHONY: test-python -test-python: +test-python: python/cog/_vendor $(PYTEST) -n auto -vv --cov=python/cog --cov-report term-missing python/tests $(if $(FILTER),-k "$(FILTER)",) .PHONY: test @@ -99,7 +104,7 @@ lint-go: $(GO) run github.com/golangci/golangci-lint/cmd/golangci-lint run ./... .PHONY: lint-python -lint-python: +lint-python: python/cog/_vendor $(RUFF) python/cog $(RUFF) format --check python @$(PYTHON) -c 'import sys; sys.exit("Warning: python >=3.10 is needed (not installed) to pass linting (pyright)") if sys.version_info < (3, 10) else None' @@ -114,12 +119,12 @@ mod-tidy: .PHONY: install-python # install dev dependencies install-python: - $(PYTHON) -m pip install '.[dev]' - + $(PIP) install '.[dev]' + $(PIP) install --no-deps --ignore-requires-python vendoring==1.2.0 .PHONY: run-docs-server run-docs-server: - pip install mkdocs-material + $(PIP) install mkdocs-material sed 's/docs\///g' README.md > ./docs/README.md cp CONTRIBUTING.md ./docs/ mkdocs serve diff --git a/pyproject.toml b/pyproject.toml index a7438613bd..115dc17f69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,22 +12,12 @@ urls."Source" = "https://github.com/replicate/cog" requires-python = ">=3.7" dependencies = [ - # intentionally loose. perhaps these should be vendored to not collide with user code? - "attrs>=20.1,<24", - "fastapi>=0.75.2,<0.99.0", - "pydantic>=1.9,<2", - "PyYAML", - "requests>=2,<3", - "structlog>=20,<25", 'typing-compat; python_version < "3.8"', - "typing_extensions>=4.4.0", - "uvicorn[standard]>=0.12,<1", ] optional-dependencies = { "dev" = [ "black", "build", - "httpx", 'hypothesis<6.80.0; python_version < "3.8"', 'hypothesis; python_version >= "3.8"', 'numpy<1.22.0; python_version < "3.8"', @@ -41,6 +31,17 @@ optional-dependencies = { "dev" = [ "pytest-cov", "responses", "ruff", + + # vendoring's dependencies are listed below. vendoring is pinned to + # python 3.8 and you can't specify --no-deps or + # --ignore-requires-python in pyproject.toml, so unfortunately we + # have to install vendoring in the Makefile. + "click", + "jsonschema", + "packaging", + "requests", + "rich", + "toml", ] } dynamic = ["version"] @@ -54,7 +55,7 @@ strictParameterNoneValue = false # legacy behavior, fixed in PEP688 disableBytesTypePromotions = true include = ["python"] -exclude = ["python/tests"] +exclude = ["python/tests", "python/cog/_vendor", "python/build"] reportMissingParameterType = "error" reportUnknownLambdaType = "error" reportUnnecessaryIsInstance = "warning" @@ -92,6 +93,7 @@ ignore = [ extend-exclude = [ "python/tests/server/fixtures/*", "test-integration/test_integration/fixtures/*", + "python/cog/_vendor/*", ] [tool.ruff.per-file-ignores] @@ -111,3 +113,42 @@ extend-exclude = [ "S607", # Starting a process with a partial executable path" "ANN", ] + +[tool.black] +exclude = '(\.eggs|\.git|\.hg|\.mypy|_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist|_vendor)' + +[tool.vendoring] +destination = "python/cog/_vendor/" +requirements = "python/cog/vendor.txt" +namespace = "cog._vendor" + +protected-files = ["__init__.py", "README.rst", "vendor.txt"] +patches-dir = "python/tools/vendoring/patches" + +[tool.vendoring.transformations] +substitute = [] +drop = [ + # we won't run any dependencies as scripts + "bin/", + + # pydantic falls back to plain python + "pydantic/*.so", + + # pyyaml falls back to plain python + "_yaml.*.so", + + # sniffio ships with tests + "sniffio/_tests", + + # h11 ships with tests + "h11/tests", + + # pydantic hypothesis plugin, fall back to python + "pydantic/_hypothesis_plugin.*.so", +] + +[tool.vendoring.typing-stubs] + +[tool.vendoring.license.directories] + +[tool.vendoring.license.fallback-urls] \ No newline at end of file diff --git a/python/cog/__init__.py b/python/cog/__init__.py index b8371e0f09..01d875ab71 100644 --- a/python/cog/__init__.py +++ b/python/cog/__init__.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel - +from ._vendor.pydantic import BaseModel from .predictor import BasePredictor from .types import ConcatenateIterator, File, Input, Path, Secret diff --git a/python/cog/files.py b/python/cog/files.py index 2ca8cd383a..9a010e1bdb 100644 --- a/python/cog/files.py +++ b/python/cog/files.py @@ -4,7 +4,7 @@ import os from urllib.parse import urlparse -import requests +from ._vendor import requests def upload_file(fh: io.IOBase, output_file_prefix: str = None) -> str: diff --git a/python/cog/json.py b/python/cog/json.py index 843572eea0..2b9e56eb3a 100644 --- a/python/cog/json.py +++ b/python/cog/json.py @@ -4,8 +4,7 @@ from types import GeneratorType from typing import Any, Callable -from pydantic import BaseModel - +from ._vendor.pydantic import BaseModel from .types import Path diff --git a/python/cog/logging.py b/python/cog/logging.py index 7b25214543..934170cb81 100644 --- a/python/cog/logging.py +++ b/python/cog/logging.py @@ -1,8 +1,8 @@ import logging import os -import structlog -from structlog.typing import EventDict +from ._vendor import structlog +from ._vendor.structlog.typing import EventDict def replace_level_with_severity( diff --git a/python/cog/predictor.py b/python/cog/predictor.py index 46f6920d20..3db5f58f17 100644 --- a/python/cog/predictor.py +++ b/python/cog/predictor.py @@ -21,22 +21,21 @@ ) from unittest.mock import patch -import structlog - import cog.code_xforms as code_xforms +from ._vendor import structlog +from ._vendor.typing_extensions import Annotated + try: from typing import get_args, get_origin except ImportError: # Python < 3.8 from typing_compat import get_args, get_origin # type: ignore -import yaml -from pydantic import BaseModel, Field, create_model -from pydantic.fields import FieldInfo +from ._vendor import yaml +from ._vendor.pydantic import BaseModel, Field, create_model +from ._vendor.pydantic.fields import FieldInfo # Added in Python 3.9. Can be from typing if we drop support for <3.9 -from typing_extensions import Annotated - from .errors import ConfigDoesNotExist, PredictorNotSet from .types import ( File as CogFile, @@ -158,7 +157,7 @@ def load_config() -> Dict[str, Any]: config_path = os.path.abspath("cog.yaml") try: with open(config_path) as fh: - config = yaml.safe_load(fh) + config: Dict[str, Any] = yaml.safe_load(fh) # type: ignore except FileNotFoundError as e: raise ConfigDoesNotExist( f"Could not find {config_path}", diff --git a/python/cog/schema.py b/python/cog/schema.py index 508c1f0f12..c7de412520 100644 --- a/python/cog/schema.py +++ b/python/cog/schema.py @@ -7,7 +7,7 @@ from enum import Enum from types import ModuleType -import pydantic +from ._vendor import pydantic BUNDLED_SCHEMA_PATH = ".cog/schema.py" diff --git a/python/cog/server/eventtypes.py b/python/cog/server/eventtypes.py index 4f9a6643a5..b333e47542 100644 --- a/python/cog/server/eventtypes.py +++ b/python/cog/server/eventtypes.py @@ -1,6 +1,6 @@ from typing import Any, Dict -from attrs import define, field, validators +from .._vendor.attrs import define, field, validators # From worker parent process diff --git a/python/cog/server/http.py b/python/cog/server/http.py index 0a2be6aaf6..ae209622ee 100644 --- a/python/cog/server/http.py +++ b/python/cog/server/http.py @@ -25,17 +25,14 @@ if TYPE_CHECKING: from typing import ParamSpec -import attrs -import structlog -import uvicorn -from fastapi import Body, FastAPI, Header, HTTPException, Path, Response -from fastapi.encoders import jsonable_encoder -from fastapi.exceptions import RequestValidationError -from fastapi.responses import JSONResponse -from pydantic import ValidationError -from pydantic.error_wrappers import ErrorWrapper - from .. import schema +from .._vendor import attrs, structlog, uvicorn +from .._vendor.fastapi import Body, FastAPI, Header, HTTPException, Path, Response +from .._vendor.fastapi.encoders import jsonable_encoder +from .._vendor.fastapi.exceptions import RequestValidationError +from .._vendor.fastapi.responses import JSONResponse +from .._vendor.pydantic import ValidationError +from .._vendor.pydantic.error_wrappers import ErrorWrapper from ..errors import PredictorNotSet from ..files import upload_file from ..json import upload_files @@ -296,7 +293,7 @@ async def predict_idempotent( "prediction ID must match the ID supplied in the URL" ), ("body", "id"), - ) + ) # type: ignore ] ) @@ -310,7 +307,7 @@ async def predict_idempotent( return _predict(request=request, respond_async=respond_async) def _predict( - *, request: PredictionRequest, respond_async: bool = False + *, request: Optional[PredictionRequest], respond_async: bool = False ) -> Response: # [compat] If no body is supplied, assume that this model can be run # with empty input. This will throw a ValidationError if that's not diff --git a/python/cog/server/runner.py b/python/cog/server/runner.py index 54e1360901..ffa3bba33a 100644 --- a/python/cog/server/runner.py +++ b/python/cog/server/runner.py @@ -7,14 +7,12 @@ from multiprocessing.pool import AsyncResult, ThreadPool from typing import Any, Callable, Optional, Tuple, Union, cast -import requests -import structlog -from attrs import define -from fastapi.encoders import jsonable_encoder -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry # type: ignore - from .. import schema, types +from .._vendor import requests, structlog +from .._vendor.attrs import define +from .._vendor.fastapi.encoders import jsonable_encoder +from .._vendor.requests.adapters import HTTPAdapter +from .._vendor.urllib3.util.retry import Retry # type: ignore from ..files import put_file_to_signed_endpoint from ..json import upload_files from .eventtypes import Done, Heartbeat, Log, PredictionOutput, PredictionOutputType @@ -454,7 +452,7 @@ def _make_file_upload_http_client() -> requests.Session: backoff_factor=0.1, status_forcelist=[408, 429, 500, 502, 503, 504], allowed_methods=["PUT"], - ), + ), # type: ignore ) session.mount("http://", adapter) session.mount("https://", adapter) diff --git a/python/cog/server/webhook.py b/python/cog/server/webhook.py index d71cf81e25..6ad961b37b 100644 --- a/python/cog/server/webhook.py +++ b/python/cog/server/webhook.py @@ -1,11 +1,9 @@ import os from typing import Any, Callable, Set -import requests -import structlog -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry # type: ignore - +from .._vendor import requests, structlog +from .._vendor.requests.adapters import HTTPAdapter +from .._vendor.urllib3.util.retry import Retry # type: ignore from ..schema import Status, WebhookEvent from .response_throttler import ResponseThrottler @@ -96,7 +94,7 @@ def requests_session_with_retries() -> requests.Session: backoff_factor=0.1, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["POST"], - ) + ) # type: ignore ) session.mount("http://", adapter) session.mount("https://", adapter) diff --git a/python/cog/types.py b/python/cog/types.py index a75b48d99b..e6d5b88e2f 100644 --- a/python/cog/types.py +++ b/python/cog/types.py @@ -8,8 +8,8 @@ import urllib.request from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union -import requests -from pydantic import Field, SecretStr +from ._vendor import requests +from ._vendor.pydantic import Field, SecretStr FILENAME_ILLEGAL_CHARS = set("\u0000/") diff --git a/python/cog/vendor.txt b/python/cog/vendor.txt new file mode 100644 index 0000000000..5da9d8b03c --- /dev/null +++ b/python/cog/vendor.txt @@ -0,0 +1,27 @@ +attrs==23.2.0 +fastapi==0.98.0 + starlette==0.27.0 + httpcore==0.17.3 + anyio==3.7.1 + httpx==0.24.1 + sniffio==1.3.1 +pydantic==1.10.15 +PyYAML==6.0.1 +requests==2.31.0 + certifi==2024.2.2 + charset-normalizer==3.3.2 + urllib3==2.0.7 +structlog==23.1.0 +typing_extensions==4.7.1 +uvicorn[standard]==0.22.0 + h11==0.14.0 + click==8.1.7 + exceptiongroup==1.2.0 + zipp==3.15.0 + idna==3.7 + importlib-metadata==6.7.0 + python-dotenv==0.21.1 + httptools==0.6.0 + websockets==11.0.3 + watchfiles==0.20.0 + uvloop==0.18.0 diff --git a/python/tests/server/conftest.py b/python/tests/server/conftest.py index b332c87c15..56eaccb5bf 100644 --- a/python/tests/server/conftest.py +++ b/python/tests/server/conftest.py @@ -6,10 +6,10 @@ from unittest import mock import pytest -from attrs import define +from cog._vendor.attrs import define from cog.command import ast_openapi_schema from cog.server.http import create_app -from fastapi.testclient import TestClient +from cog._vendor.fastapi.testclient import TestClient @define diff --git a/python/tests/server/fixtures/complex_output.py b/python/tests/server/fixtures/complex_output.py index 3be75bd2a6..db0f6fdfaa 100644 --- a/python/tests/server/fixtures/complex_output.py +++ b/python/tests/server/fixtures/complex_output.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from cog import BaseModel class Output(BaseModel): diff --git a/python/tests/server/fixtures/input_unsupported_type.py b/python/tests/server/fixtures/input_unsupported_type.py index 10518d659f..f33f1564e6 100644 --- a/python/tests/server/fixtures/input_unsupported_type.py +++ b/python/tests/server/fixtures/input_unsupported_type.py @@ -1,5 +1,4 @@ -from cog import BasePredictor -from pydantic import BaseModel +from cog import BasePredictor, BaseModel class Input(BaseModel): diff --git a/python/tests/server/fixtures/openapi_custom_output_type.py b/python/tests/server/fixtures/openapi_custom_output_type.py index f04ed1e5ea..ed90ebeee4 100644 --- a/python/tests/server/fixtures/openapi_custom_output_type.py +++ b/python/tests/server/fixtures/openapi_custom_output_type.py @@ -1,5 +1,4 @@ -from cog import BasePredictor -from pydantic import BaseModel +from cog import BasePredictor, BaseModel # Calling this `MyOutput` to test if cog renames it to `Output` in the schema diff --git a/python/tests/server/fixtures/openapi_output_type.py b/python/tests/server/fixtures/openapi_output_type.py index a22d6aa2f8..3df784d179 100644 --- a/python/tests/server/fixtures/openapi_output_type.py +++ b/python/tests/server/fixtures/openapi_output_type.py @@ -1,5 +1,4 @@ -from cog import BasePredictor -from pydantic import BaseModel +from cog import BasePredictor, BaseModel # An output object called `Output` needs to be special cased because pydantic tries to dedupe it with the internal `Output` diff --git a/python/tests/server/fixtures/output_complex.py b/python/tests/server/fixtures/output_complex.py index 0a38809114..74c2bcca41 100644 --- a/python/tests/server/fixtures/output_complex.py +++ b/python/tests/server/fixtures/output_complex.py @@ -1,7 +1,6 @@ import io -from cog import BasePredictor, File -from pydantic import BaseModel +from cog import BasePredictor, File, BaseModel class Output(BaseModel): diff --git a/python/tests/server/fixtures/output_iterator_complex.py b/python/tests/server/fixtures/output_iterator_complex.py index 737093a628..030a4998aa 100644 --- a/python/tests/server/fixtures/output_iterator_complex.py +++ b/python/tests/server/fixtures/output_iterator_complex.py @@ -1,7 +1,6 @@ from typing import Iterator, List -from cog import BasePredictor -from pydantic import BaseModel +from cog import BasePredictor, BaseModel class Output(BaseModel): diff --git a/python/tests/server/test_http_input.py b/python/tests/server/test_http_input.py index a64bb0104f..16cf548f3b 100644 --- a/python/tests/server/test_http_input.py +++ b/python/tests/server/test_http_input.py @@ -10,6 +10,8 @@ from .conftest import uses_predictor +responses.mock.target = "cog._vendor.requests.adapters.HTTPAdapter.send" + @uses_predictor("input_none") def test_no_input(client, match): diff --git a/python/tests/server/test_http_output.py b/python/tests/server/test_http_output.py index 281134cf9e..534dcf49b1 100644 --- a/python/tests/server/test_http_output.py +++ b/python/tests/server/test_http_output.py @@ -6,6 +6,8 @@ from .conftest import uses_predictor, uses_predictor_with_client_options +responses.mock.target = "cog._vendor.requests.adapters.HTTPAdapter.send" + @uses_predictor("output_wrong_type") def test_return_wrong_type(client): diff --git a/python/tests/server/test_webhook.py b/python/tests/server/test_webhook.py index 8a6bfb8543..9f2a102aab 100644 --- a/python/tests/server/test_webhook.py +++ b/python/tests/server/test_webhook.py @@ -1,4 +1,4 @@ -import requests +from cog._vendor import requests import responses from cog.schema import WebhookEvent from cog.server.webhook import webhook_caller, webhook_caller_filtered diff --git a/python/tests/server/test_worker.py b/python/tests/server/test_worker.py index 2895e0c81f..0cdd5b970e 100644 --- a/python/tests/server/test_worker.py +++ b/python/tests/server/test_worker.py @@ -3,7 +3,7 @@ from typing import Any, Optional import pytest -from attrs import define +from cog._vendor.attrs import define from cog.server.eventtypes import ( Done, Heartbeat, diff --git a/python/tests/test_json.py b/python/tests/test_json.py index 6311e34be1..7adefba870 100644 --- a/python/tests/test_json.py +++ b/python/tests/test_json.py @@ -5,7 +5,7 @@ import numpy as np from cog.files import upload_file from cog.json import make_encodeable, upload_files -from pydantic import BaseModel +from cog._vendor.pydantic import BaseModel def test_make_encodeable_recursively_encodes_tuples(): diff --git a/python/tools/vendoring/patches/anyio.patch b/python/tools/vendoring/patches/anyio.patch new file mode 100644 index 0000000000..52e70232e4 --- /dev/null +++ b/python/tools/vendoring/patches/anyio.patch @@ -0,0 +1,20 @@ +--- ./python/cog/_vendor/anyio/_core/_eventloop.py 2024-04-16 13:10:15 ++++ ./python/cog/_vendor/anyio/_core/_eventloop.py.new 2024-04-16 13:11:09 +@@ -134,7 +134,7 @@ + + @contextmanager + def claim_worker_thread(backend: str) -> Generator[Any, None, None]: +- module = sys.modules["anyio._backends._" + backend] ++ module = sys.modules["cog._vendor.anyio._backends._" + backend] + threadlocals.current_async_module = module + try: + yield +@@ -146,7 +146,7 @@ + if asynclib_name is None: + asynclib_name = sniffio.current_async_library() + +- modulename = "anyio._backends._" + asynclib_name ++ modulename = "cog._vendor.anyio._backends._" + asynclib_name + try: + return sys.modules[modulename] + except KeyError: diff --git a/python/tools/vendoring/patches/certifi.patch b/python/tools/vendoring/patches/certifi.patch new file mode 100644 index 0000000000..09e9168e8a --- /dev/null +++ b/python/tools/vendoring/patches/certifi.patch @@ -0,0 +1,42 @@ +--- ./python/cog/_vendor/certifi/core.py 2024-04-15 18:08:25 ++++ ./python/cog/_vendor/certifi/core.py.new 2024-04-15 18:21:12 +@@ -37,14 +37,14 @@ + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. +- _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) ++ _CACERT_CTX = as_file(files("cog._vendor.certifi").joinpath("cacert.pem")) + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: +- return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") ++ return files("cog._vendor.certifi").joinpath("cacert.pem").read_text(encoding="ascii") + + elif sys.version_info >= (3, 7): + +@@ -73,14 +73,14 @@ + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. +- _CACERT_CTX = get_path("certifi", "cacert.pem") ++ _CACERT_CTX = get_path("cog._vendor.certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: +- return read_text("certifi", "cacert.pem", encoding="ascii") ++ return read_text("cog._vendor.certifi", "cacert.pem", encoding="ascii") + + else: + import os +@@ -111,4 +111,4 @@ + return os.path.join(f, "cacert.pem") + + def contents() -> str: +- return read_text("certifi", "cacert.pem", encoding="ascii") ++ return read_text("cog._vendor.certifi", "cacert.pem", encoding="ascii") diff --git a/python/tools/vendoring/patches/pydantic.patch b/python/tools/vendoring/patches/pydantic.patch new file mode 100644 index 0000000000..0fd5d700c3 --- /dev/null +++ b/python/tools/vendoring/patches/pydantic.patch @@ -0,0 +1,96 @@ +--- ./python/cog/_vendor/pydantic/_hypothesis_plugin.py 2024-04-15 16:14:31 ++++ ./python/cog/_vendor/pydantic/_hypothesis_plugin.py.new 2024-04-15 16:16:29 +@@ -33,8 +33,8 @@ + import hypothesis.strategies as st + + import pydantic +-import pydantic.color +-import pydantic.types ++import pydantic.color as pydantic_color ++import pydantic.types as pydantic_types + from pydantic.utils import lenient_issubclass + + # FilePath and DirectoryPath are explicitly unsupported, as we'd have to create +@@ -88,23 +88,23 @@ + _color_regexes = ( + '|'.join( + ( +- pydantic.color.r_hex_short, +- pydantic.color.r_hex_long, +- pydantic.color.r_rgb, +- pydantic.color.r_rgba, +- pydantic.color.r_hsl, +- pydantic.color.r_hsla, ++ pydantic_color.r_hex_short, ++ pydantic_color.r_hex_long, ++ pydantic_color.r_rgb, ++ pydantic_color.r_rgba, ++ pydantic_color.r_hsl, ++ pydantic_color.r_hsla, + ) + ) + # Use more precise regex patterns to avoid value-out-of-range errors +- .replace(pydantic.color._r_sl, r'(?:(\d\d?(?:\.\d+)?|100(?:\.0+)?)%)') +- .replace(pydantic.color._r_alpha, r'(?:(0(?:\.\d+)?|1(?:\.0+)?|\.\d+|\d{1,2}%))') +- .replace(pydantic.color._r_255, r'(?:((?:\d|\d\d|[01]\d\d|2[0-4]\d|25[0-4])(?:\.\d+)?|255(?:\.0+)?))') ++ .replace(pydantic_color._r_sl, r'(?:(\d\d?(?:\.\d+)?|100(?:\.0+)?)%)') ++ .replace(pydantic_color._r_alpha, r'(?:(0(?:\.\d+)?|1(?:\.0+)?|\.\d+|\d{1,2}%))') ++ .replace(pydantic_color._r_255, r'(?:((?:\d|\d\d|[01]\d\d|2[0-4]\d|25[0-4])(?:\.\d+)?|255(?:\.0+)?))') + ) + st.register_type_strategy( +- pydantic.color.Color, ++ pydantic_color.Color, + st.one_of( +- st.sampled_from(sorted(pydantic.color.COLORS_BY_NAME)), ++ st.sampled_from(sorted(pydantic_color.COLORS_BY_NAME)), + st.tuples( + st.integers(0, 255), + st.integers(0, 255), +@@ -182,22 +182,22 @@ + + + @overload +-def _registered(typ: Type[pydantic.types.T]) -> Type[pydantic.types.T]: ++def _registered(typ: Type[pydantic_types.T]) -> Type[pydantic_types.T]: + pass + + + @overload +-def _registered(typ: pydantic.types.ConstrainedNumberMeta) -> pydantic.types.ConstrainedNumberMeta: ++def _registered(typ: pydantic_types.ConstrainedNumberMeta) -> pydantic_types.ConstrainedNumberMeta: + pass + + + def _registered( +- typ: Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta] +-) -> Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta]: +- # This function replaces the version in `pydantic.types`, in order to ++ typ: Union[Type[pydantic_types.T], pydantic_types.ConstrainedNumberMeta] ++) -> Union[Type[pydantic_types.T], pydantic_types.ConstrainedNumberMeta]: ++ # This function replaces the version in `pydantic_types`, in order to + # effect the registration of new constrained types so that Hypothesis + # can generate valid examples. +- pydantic.types._DEFINED_TYPES.add(typ) ++ pydantic_types._DEFINED_TYPES.add(typ) + for supertype, resolver in RESOLVERS.items(): + if issubclass(typ, supertype): + st.register_type_strategy(typ, resolver(typ)) # type: ignore +@@ -206,7 +206,7 @@ + + + def resolves( +- typ: Union[type, pydantic.types.ConstrainedNumberMeta] ++ typ: Union[type, pydantic_types.ConstrainedNumberMeta] + ) -> Callable[[Callable[..., st.SearchStrategy]], Callable[..., st.SearchStrategy]]: # type: ignore[type-arg] + def inner(f): # type: ignore + assert f not in RESOLVERS +@@ -385,7 +385,7 @@ + + + # Finally, register all previously-defined types, and patch in our new function +-for typ in list(pydantic.types._DEFINED_TYPES): ++for typ in list(pydantic_types._DEFINED_TYPES): + _registered(typ) +-pydantic.types._registered = _registered ++pydantic_types._registered = _registered + st.register_type_strategy(pydantic.Json, resolve_json) diff --git a/python/tools/vendoring/patches/requests.patch b/python/tools/vendoring/patches/requests.patch new file mode 100644 index 0000000000..7b32e8c340 --- /dev/null +++ b/python/tools/vendoring/patches/requests.patch @@ -0,0 +1,39 @@ +--- ./python/cog/_vendor/requests/packages.py 2024-04-15 18:07:15 ++++ ./python/cog/_vendor/requests/packages.py.new 2024-04-15 18:08:05 +@@ -1,28 +1,20 @@ + import sys + +-try: +- import chardet +-except ImportError: +- import warnings ++import warnings ++from cog._vendor import charset_normalizer as chardet ++warnings.filterwarnings("ignore", "Trying to detect", module="charset_normalizer") + +- import charset_normalizer as chardet +- +- warnings.filterwarnings("ignore", "Trying to detect", module="charset_normalizer") +- + # This code exists for backwards compatibility reasons. + # I don't like it either. Just look the other way. :) + + for package in ("urllib3", "idna"): +- locals()[package] = __import__(package) ++ vendored_package = "cog._vendor." + package ++ locals()[package] = __import__(vendored_package) + # This traversal is apparently necessary such that the identities are + # preserved (requests.packages.urllib3.* is urllib3.*) + for mod in list(sys.modules): +- if mod == package or mod.startswith(f"{package}."): +- sys.modules[f"requests.packages.{mod}"] = sys.modules[mod] ++ if mod == vendored_package or mod.startswith(vendored_package + '.'): ++ unprefixed_mod = mod[len("cog._vendor."):] ++ sys.modules['cog._vendor.requests.packages.' + unprefixed_mod] = sys.modules[mod] + +-target = chardet.__name__ +-for mod in list(sys.modules): +- if mod == target or mod.startswith(f"{target}."): +- target = target.replace(target, "chardet") +- sys.modules[f"requests.packages.{target}"] = sys.modules[mod] + # Kinda cool, though, right? diff --git a/python/tools/vendoring/patches/starlette.patch b/python/tools/vendoring/patches/starlette.patch new file mode 100644 index 0000000000..18463cf109 --- /dev/null +++ b/python/tools/vendoring/patches/starlette.patch @@ -0,0 +1,29 @@ +--- ./python/cog/_vendor/starlette/testclient.py 2023-04-17 13:38:36 ++++ ./python/cog/_vendor/starlette/testclient.py 2023-04-17 13:45:59 +@@ -12,7 +12,7 @@ + from urllib.parse import unquote, urljoin + + import anyio +-import anyio.from_thread ++import anyio.from_thread as from_thread + import httpx + from anyio.streams.stapled import StapledObjectStream + +@@ -410,7 +410,7 @@ + if self.portal is not None: + yield self.portal + else: +- with anyio.from_thread.start_blocking_portal( ++ with from_thread.start_blocking_portal( + **self.async_backend + ) as portal: + yield portal +@@ -728,7 +728,7 @@ + def __enter__(self) -> "TestClient": + with contextlib.ExitStack() as stack: + self.portal = portal = stack.enter_context( +- anyio.from_thread.start_blocking_portal(**self.async_backend) ++ from_thread.start_blocking_portal(**self.async_backend) + ) + + @stack.callback diff --git a/python/tools/vendoring/patches/urllib3.patch b/python/tools/vendoring/patches/urllib3.patch new file mode 100644 index 0000000000..c881a036a3 --- /dev/null +++ b/python/tools/vendoring/patches/urllib3.patch @@ -0,0 +1,29 @@ +diff --git a/python/cog/_vendor/urllib3/contrib/securetransport.py b/python/cog/_vendor/urllib3/contrib/securetransport.py +index b97555454..189132baa 100644 +--- a/python/cog/_vendor/urllib3/contrib/securetransport.py ++++ b/python/cog/_vendor/urllib3/contrib/securetransport.py +@@ -19,8 +19,8 @@ + + To use this module, simply import and inject it:: + +- import urllib3.contrib.securetransport +- urllib3.contrib.securetransport.inject_into_urllib3() ++ import urllib3.contrib.securetransport as securetransport ++ securetransport.inject_into_urllib3() + + Happy TLSing! + +diff --git a/python/cog/_vendor/urllib3/contrib/pyopenssl.py b/python/cog/_vendor/urllib3/contrib/pyopenssl.py +index c43146279..4cded53f6 100644 +--- a/python/cog/_vendor/urllib3/contrib/pyopenssl.py ++++ b/python/cog/_vendor/urllib3/contrib/pyopenssl.py +@@ -28,7 +28,7 @@ + .. code-block:: python + + try: +- import urllib3.contrib.pyopenssl +- urllib3.contrib.pyopenssl.inject_into_urllib3() ++ import urllib3.contrib.pyopenssl as pyopenssl ++ pyopenssl.inject_into_urllib3() + except ImportError: + pass diff --git a/python/tools/vendoring/patches/uvicorn.patch b/python/tools/vendoring/patches/uvicorn.patch new file mode 100644 index 0000000000..51e557c1ee --- /dev/null +++ b/python/tools/vendoring/patches/uvicorn.patch @@ -0,0 +1,13 @@ +--- ./python/cog/_vendor/uvicorn/importer.py 2024-04-15 21:38:17 ++++ ./python/cog/_vendor/uvicorn/importer.py.new 2024-04-15 21:41:46 +@@ -18,6 +18,10 @@ + raise ImportFromStringError(message.format(import_str=import_str)) + + try: ++ if module_str == "uvicorn": ++ module_str = "cog._vendor.uvicorn" ++ if module_str.startswith("uvicorn."): ++ module_str = "cog._vendor.uvicorn." + module_str.split("uvicorn.", 1)[1] + module = importlib.import_module(module_str) + except ImportError as exc: + if exc.name != module_str: diff --git a/test-integration/test_integration/fixtures/complex_output_project/predict.py b/test-integration/test_integration/fixtures/complex_output_project/predict.py index b1fb8d0e14..2076d83691 100644 --- a/test-integration/test_integration/fixtures/complex_output_project/predict.py +++ b/test-integration/test_integration/fixtures/complex_output_project/predict.py @@ -1,8 +1,7 @@ import io -from cog import BasePredictor, Path +from cog import BasePredictor, Path, BaseModel from typing import Optional -from pydantic import BaseModel class ModelOutput(BaseModel):