diff --git a/src/prisma/__init__.py b/src/prisma/__init__.py index 1ca480d54..b01065f6a 100644 --- a/src/prisma/__init__.py +++ b/src/prisma/__init__.py @@ -6,7 +6,8 @@ __copyright__ = 'Copyright 2020-2023 RobertCraigie' __version__ = '0.15.0' -from typing import TYPE_CHECKING +import sys as _sys +from typing import TYPE_CHECKING, Union from . import errors as errors from .utils import setup_logging @@ -19,6 +20,13 @@ ) from .validator import * +try: + from .metadata import GENERATED_VERSION as _generated_version # noqa: TID251 + + GENERATED_VERSION: Union[str, None] = _generated_version +except ImportError: + GENERATED_VERSION = None # pyright: ignore[reportConstantRedefinition] + # the import ordering is important here because # we rely on the fact that `prisma/client.py` is the # first piece of generated code that is loaded. This is @@ -49,7 +57,6 @@ def __getattr__(name: str): except KeyError as err: # TODO: support checking for 'models' here too if name in {'Prisma', 'Client'}: - # TODO: remove this frame from the stack trace raise RuntimeError( "The Client hasn't been generated yet, " 'you must run `prisma generate` before you can use the client.\n' @@ -58,6 +65,10 @@ def __getattr__(name: str): # leaves handling of this potential error to Python as per PEP 562 raise AttributeError() from err +except Exception as exc: + if GENERATED_VERSION == __version__: + raise + print('ignoring error during generated client import as the generated client is outdated', file=_sys.stderr) # noqa: T201 setup_logging() diff --git a/src/prisma/_base_client.py b/src/prisma/_base_client.py index ed604f0ff..fe69df0fc 100644 --- a/src/prisma/_base_client.py +++ b/src/prisma/_base_client.py @@ -186,7 +186,7 @@ def __del__(self) -> None: # client as well as the transaction client the original client cannot # be `free`d before the transaction is finished. So stopping the engine # here should be safe. - if self._internal_engine is not None and not self._copied: + if hasattr(self, '_internal_engine') and self._internal_engine is not None and not self._copied: log.debug('unclosed client - stopping engine') engine = self._internal_engine self._internal_engine = None diff --git a/src/prisma/generator/models.py b/src/prisma/generator/models.py index 40e48eeb8..693a797ba 100644 --- a/src/prisma/generator/models.py +++ b/src/prisma/generator/models.py @@ -351,9 +351,12 @@ def parse_obj(cls, obj: Any) -> 'GenericData[ConfigT]': def to_params(self) -> Dict[str, Any]: """Get the parameters that should be sent to Jinja templates""" + from .. import __version__ + params = vars(self) params['type_schema'] = Schema.from_data(self) params['client_types'] = ClientTypes.from_data(self) + params['generated_version'] = __version__ # add utility functions for func in [ diff --git a/src/prisma/generator/templates/client.py.jinja b/src/prisma/generator/templates/client.py.jinja index 6aa0bed51..3de1d92f3 100644 --- a/src/prisma/generator/templates/client.py.jinja +++ b/src/prisma/generator/templates/client.py.jinja @@ -11,7 +11,7 @@ from typing_extensions import override from pydantic import BaseModel -from . import types, models, errors, actions +from . import types, models, errors, actions, __version__, metadata from ._base_client import BasePrisma, UseClientDefault, USE_CLIENT_DEFAULT from .types import DatasourceOverride, HttpConfig, MetricsFormat from ._types import BaseModelT, PrismaMethod, TransactionId, Datasource @@ -77,6 +77,11 @@ class Prisma({% if is_async %}AsyncBasePrisma{% else %}SyncBasePrisma{% endif %} connect_timeout: int | timedelta = DEFAULT_CONNECT_TIMEOUT, http: HttpConfig | None = None, ) -> None: + if metadata.GENERATED_VERSION != __version__: + raise RuntimeError( + f'outdated generated client, the client is disabled until `prisma generate` is ran again; generated version={metadata.GENERATED_VERSION} package version={__version__}' + ) + super().__init__( http=http, use_dotenv=use_dotenv, diff --git a/src/prisma/generator/templates/metadata.py.jinja b/src/prisma/generator/templates/metadata.py.jinja index 50af2b9b5..09c6a1d68 100644 --- a/src/prisma/generator/templates/metadata.py.jinja +++ b/src/prisma/generator/templates/metadata.py.jinja @@ -3,6 +3,7 @@ from __future__ import annotations # fmt: off # -- template metadata.py.jinja -- +GENERATED_VERSION: str = '{{ generated_version }}' PRISMA_MODELS: set[str] = { {% for model in dmmf.datamodel.models %} diff --git a/tests/test_client.py b/tests/test_client.py index f70d9eebd..f995ce9f1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,7 +8,7 @@ from mock import AsyncMock from pytest_mock import MockerFixture -from prisma import ENGINE_TYPE, SCHEMA_PATH, Prisma, errors, get_client +from prisma import ENGINE_TYPE, SCHEMA_PATH, Prisma, errors, metadata, get_client from prisma.types import HttpConfig from prisma.testing import reset_client from prisma.cli.prisma import run @@ -255,3 +255,15 @@ def test_is_registered(client: Prisma) -> None: with reset_client(): assert not client.is_registered() assert not other_client.is_registered() + + +def test_version_mismatch() -> None: + old_version = metadata.GENERATED_VERSION + + try: + metadata.GENERATED_VERSION = '0.10.4' + + with pytest.raises(RuntimeError, match='outdated generated client'): + Prisma() + finally: + metadata.GENERATED_VERSION = old_version diff --git a/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_async[client.py].raw b/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_async[client.py].raw index c4fc5fcb6..50ccb09e5 100644 --- a/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_async[client.py].raw +++ b/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_async[client.py].raw @@ -49,7 +49,7 @@ from typing_extensions import override from pydantic import BaseModel -from . import types, models, errors, actions +from . import types, models, errors, actions, __version__, metadata from ._base_client import BasePrisma, UseClientDefault, USE_CLIENT_DEFAULT from .types import DatasourceOverride, HttpConfig, MetricsFormat from ._types import BaseModelT, PrismaMethod, TransactionId, Datasource @@ -133,6 +133,11 @@ class Prisma(AsyncBasePrisma): connect_timeout: int | timedelta = DEFAULT_CONNECT_TIMEOUT, http: HttpConfig | None = None, ) -> None: + if metadata.GENERATED_VERSION != __version__: + raise RuntimeError( + f'outdated generated client, the client is disabled until `prisma generate` is ran again; generated version={metadata.GENERATED_VERSION} package version={__version__}' + ) + super().__init__( http=http, use_dotenv=use_dotenv, diff --git a/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_async[metadata.py].raw b/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_async[metadata.py].raw index feb2a9372..fe20a3a16 100644 --- a/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_async[metadata.py].raw +++ b/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_async[metadata.py].raw @@ -5,6 +5,7 @@ from __future__ import annotations # fmt: off # -- template metadata.py.jinja -- +GENERATED_VERSION: str = '0.15.0' PRISMA_MODELS: set[str] = { 'Post', diff --git a/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_sync[client.py].raw b/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_sync[client.py].raw index 2f0f81bb6..0f355a28c 100644 --- a/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_sync[client.py].raw +++ b/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_sync[client.py].raw @@ -49,7 +49,7 @@ from typing_extensions import override from pydantic import BaseModel -from . import types, models, errors, actions +from . import types, models, errors, actions, __version__, metadata from ._base_client import BasePrisma, UseClientDefault, USE_CLIENT_DEFAULT from .types import DatasourceOverride, HttpConfig, MetricsFormat from ._types import BaseModelT, PrismaMethod, TransactionId, Datasource @@ -133,6 +133,11 @@ class Prisma(SyncBasePrisma): connect_timeout: int | timedelta = DEFAULT_CONNECT_TIMEOUT, http: HttpConfig | None = None, ) -> None: + if metadata.GENERATED_VERSION != __version__: + raise RuntimeError( + f'outdated generated client, the client is disabled until `prisma generate` is ran again; generated version={metadata.GENERATED_VERSION} package version={__version__}' + ) + super().__init__( http=http, use_dotenv=use_dotenv, diff --git a/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_sync[metadata.py].raw b/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_sync[metadata.py].raw index feb2a9372..fe20a3a16 100644 --- a/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_sync[metadata.py].raw +++ b/tests/test_generation/exhaustive/__snapshots__/test_exhaustive/test_sync[metadata.py].raw @@ -5,6 +5,7 @@ from __future__ import annotations # fmt: off # -- template metadata.py.jinja -- +GENERATED_VERSION: str = '0.15.0' PRISMA_MODELS: set[str] = { 'Post',