From beca589d73ece015797f76df151fd9a0558fef4d Mon Sep 17 00:00:00 2001 From: Antoine Jeannot Date: Wed, 22 Nov 2023 14:58:14 +0100 Subject: [PATCH 1/5] fix pytest ini options --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index af4714a5..ad1f45ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,12 +119,10 @@ modelkit = ["py.typed"] [tool.pytest.ini_options] addopts = """ ---strict +--strict-markers --verbose ---tb=native -vv --failed-first ---disable-warnings --durations 10 --color=yes tests""" From f011f3e1b2e2f2658f889f183474d4c05be78b02 Mon Sep 17 00:00:00 2001 From: Antoine Jeannot Date: Mon, 20 Nov 2023 21:41:28 +0100 Subject: [PATCH 2/5] bump pydantic --- .pre-commit-config.yaml | 2 +- modelkit/api.py | 2 +- modelkit/assets/drivers/abc.py | 15 ++- modelkit/assets/drivers/azure.py | 9 +- modelkit/assets/drivers/gcs.py | 9 +- modelkit/assets/drivers/local.py | 4 +- modelkit/assets/drivers/s3.py | 38 ++++-- modelkit/core/errors.py | 2 +- modelkit/core/library.py | 4 +- modelkit/core/model.py | 15 +-- modelkit/core/model_configuration.py | 20 +-- modelkit/core/models/distant_model.py | 8 +- modelkit/core/settings.py | 144 ++++++++++++++++------ modelkit/core/types.py | 2 +- modelkit/testing/fixtures.py | 10 +- modelkit/utils/cache.py | 2 +- modelkit/utils/pretty.py | 7 +- modelkit/utils/pydantic.py | 32 ----- pyproject.toml | 5 +- requirements-dev.txt | 16 ++- tests/conftest.py | 1 - tests/test_caching.py | 20 ++- tests/test_describe.py | 42 ++++--- tests/test_model_serialization.py | 2 +- tests/test_settings.py | 24 ++++ tests/test_validate.py | 168 +++++++------------------- tests/testdata/library_describe.txt | 30 ++--- 27 files changed, 334 insertions(+), 299 deletions(-) delete mode 100644 modelkit/utils/pydantic.py create mode 100644 tests/test_settings.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01c4623f..f8cb1e93 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: tests/.* )$ additional_dependencies: [ - pydantic==1.*, + pydantic==2.*, types-python-dateutil, types-requests, types-urllib3, diff --git a/modelkit/api.py b/modelkit/api.py index edf56688..d992f00c 100644 --- a/modelkit/api.py +++ b/modelkit/api.py @@ -98,7 +98,7 @@ def __init__( logger.info("Adding model", name=model_name) item_type = m._item_type or Any try: - item_type.schema() # type: ignore + item_type.model_json_schema() # type: ignore except (ValueError, AttributeError): logger.info( "Discarding item type info for model", name=model_name, path=path diff --git a/modelkit/assets/drivers/abc.py b/modelkit/assets/drivers/abc.py index b2bdd9fd..45dd13b6 100644 --- a/modelkit/assets/drivers/abc.py +++ b/modelkit/assets/drivers/abc.py @@ -3,13 +3,18 @@ import pydantic +from modelkit.core.settings import ModelkitSettings -class StorageDriverSettings(pydantic.BaseSettings): - bucket: str = pydantic.Field(..., env="MODELKIT_STORAGE_BUCKET") - lazy_driver: bool = pydantic.Field(False, env="MODELKIT_LAZY_DRIVER") - class Config: - extra = "allow" +class StorageDriverSettings(ModelkitSettings): + bucket: str = pydantic.Field( + ..., validation_alias=pydantic.AliasChoices("bucket", "MODELKIT_STORAGE_BUCKET") + ) + lazy_driver: bool = pydantic.Field( + False, + validation_alias=pydantic.AliasChoices("lazy_driver", "MODELKIT_LAZY_DRIVER"), + ) + model_config = pydantic.ConfigDict(extra="allow") class StorageDriver(abc.ABC): diff --git a/modelkit/assets/drivers/azure.py b/modelkit/assets/drivers/azure.py index 1003a8b8..3e293aaa 100644 --- a/modelkit/assets/drivers/azure.py +++ b/modelkit/assets/drivers/azure.py @@ -17,11 +17,12 @@ class AzureStorageDriverSettings(StorageDriverSettings): connection_string: Optional[str] = pydantic.Field( - None, env="AZURE_STORAGE_CONNECTION_STRING" + None, + validation_alias=pydantic.AliasChoices( + "connection_string", "AZURE_STORAGE_CONNECTION_STRING" + ), ) - - class Config: - extra = "forbid" + model_config = pydantic.ConfigDict(extra="forbid") class AzureStorageDriver(StorageDriver): diff --git a/modelkit/assets/drivers/gcs.py b/modelkit/assets/drivers/gcs.py index 7e869295..4d93b656 100644 --- a/modelkit/assets/drivers/gcs.py +++ b/modelkit/assets/drivers/gcs.py @@ -19,11 +19,12 @@ class GCSStorageDriverSettings(StorageDriverSettings): service_account_path: Optional[str] = pydantic.Field( - None, env="GOOGLE_APPLICATION_CREDENTIALS" + None, + validation_alias=pydantic.AliasChoices( + "service_account_path", "GOOGLE_APPLICATION_CREDENTIALS" + ), ) - - class Config: - extra = "forbid" + model_config = pydantic.ConfigDict(extra="forbid") class GCSStorageDriver(StorageDriver): diff --git a/modelkit/assets/drivers/local.py b/modelkit/assets/drivers/local.py index 68839383..37c39e16 100644 --- a/modelkit/assets/drivers/local.py +++ b/modelkit/assets/drivers/local.py @@ -3,6 +3,7 @@ import shutil from typing import Dict, Optional, Union +import pydantic from structlog import get_logger from modelkit.assets import errors @@ -12,8 +13,7 @@ class LocalStorageDriverSettings(StorageDriverSettings): - class Config: - extra = "forbid" + model_config = pydantic.ConfigDict(extra="forbid") class LocalStorageDriver(StorageDriver): diff --git a/modelkit/assets/drivers/s3.py b/modelkit/assets/drivers/s3.py index f6fc995b..10f051ca 100644 --- a/modelkit/assets/drivers/s3.py +++ b/modelkit/assets/drivers/s3.py @@ -17,17 +17,37 @@ class S3StorageDriverSettings(StorageDriverSettings): - aws_access_key_id: Optional[str] = pydantic.Field(None, env="AWS_ACCESS_KEY_ID") + aws_access_key_id: Optional[str] = pydantic.Field( + None, + validation_alias=pydantic.AliasChoices( + "aws_access_key_id", "AWS_ACCESS_KEY_ID" + ), + ) aws_secret_access_key: Optional[str] = pydantic.Field( - None, env="AWS_SECRET_ACCESS_KEY" + None, + validation_alias=pydantic.AliasChoices( + "aws_secret_access_key", "AWS_SECRET_ACCESS_KEY" + ), ) - aws_default_region: Optional[str] = pydantic.Field(None, env="AWS_DEFAULT_REGION") - aws_session_token: Optional[str] = pydantic.Field(None, env="AWS_SESSION_TOKEN") - s3_endpoint: Optional[str] = pydantic.Field(None, env="S3_ENDPOINT") - aws_kms_key_id: Optional[str] = pydantic.Field(None, env="AWS_KMS_KEY_ID") - - class Config: - extra = "forbid" + aws_default_region: Optional[str] = pydantic.Field( + None, + validation_alias=pydantic.AliasChoices( + "aws_default_region", "AWS_DEFAULT_REGION" + ), + ) + aws_session_token: Optional[str] = pydantic.Field( + None, + validation_alias=pydantic.AliasChoices( + "aws_session_token", "AWS_SESSION_TOKEN" + ), + ) + s3_endpoint: Optional[str] = pydantic.Field( + None, validation_alias=pydantic.AliasChoices("s3_endpoint", "S3_ENDPOINT") + ) + aws_kms_key_id: Optional[str] = pydantic.Field( + None, validation_alias=pydantic.AliasChoices("aws_kms_key_id", "AWS_KMS_KEY_ID") + ) + model_config = pydantic.ConfigDict(extra="forbid") class S3StorageDriver(StorageDriver): diff --git a/modelkit/core/errors.py b/modelkit/core/errors.py index 3e6c550b..e644c1d4 100644 --- a/modelkit/core/errors.py +++ b/modelkit/core/errors.py @@ -23,7 +23,7 @@ class ModelkitDataValidationException(Exception): def __init__( self, model_identifier: str, - pydantic_exc: Optional[pydantic.error_wrappers.ValidationError] = None, + pydantic_exc: Optional[pydantic.ValidationError] = None, error_str: str = "Data validation error in model", ): pydantic_exc_output = "" diff --git a/modelkit/core/library.py b/modelkit/core/library.py index a8154be1..0480f45c 100644 --- a/modelkit/core/library.py +++ b/modelkit/core/library.py @@ -53,7 +53,7 @@ class ConfigurationNotFoundException(Exception): class AssetInfo(pydantic.BaseModel): path: str - version: Optional[str] + version: Optional[str] = None class ModelLibrary: @@ -68,7 +68,7 @@ def __init__( required_models: Optional[Union[List[str], Dict[str, Any]]] = None, ): """ - Create a prediction service + Create a model library :param models: a `Model` class, a module, or a list of either in which the ModelLibrary will look for configurations. diff --git a/modelkit/core/model.py b/modelkit/core/model.py index 5ed5d8f5..d72aafbd 100644 --- a/modelkit/core/model.py +++ b/modelkit/core/model.py @@ -36,7 +36,6 @@ from modelkit.utils.cache import Cache, CacheItem from modelkit.utils.memory import PerformanceTracker from modelkit.utils.pretty import describe, pretty_print_type -from modelkit.utils.pydantic import construct_recursive logger = get_logger(__name__) @@ -182,11 +181,8 @@ def _load(self) -> None: class InternalDataModel(pydantic.BaseModel): - data: Any - - class Config: - arbitrary_types_allowed = True - extra = "forbid" + data: Any = None + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True, extra="forbid") PYDANTIC_ERROR_TRUNCATION = 20 @@ -392,11 +388,8 @@ def _validate( ): if model: try: - if self.service_settings.enable_validation: - return model(data=item).data - else: - return construct_recursive(model, data=item).data - except pydantic.error_wrappers.ValidationError as exc: + return model(data=item).data + except pydantic.ValidationError as exc: raise exception( f"{self.__class__.__name__}[{self.configuration_key}]", pydantic_exc=exc, diff --git a/modelkit/core/model_configuration.py b/modelkit/core/model_configuration.py index b618b802..68e5b645 100644 --- a/modelkit/core/model_configuration.py +++ b/modelkit/core/model_configuration.py @@ -10,20 +10,24 @@ from structlog import get_logger from modelkit.core.model import Asset +from modelkit.core.settings import ModelkitSettings from modelkit.core.types import LibraryModelsType logger = get_logger(__name__) -class ModelConfiguration(pydantic.BaseSettings): +class ModelConfiguration(ModelkitSettings): model_type: Type[Asset] - asset: Optional[str] + asset: Optional[str] = None model_settings: Optional[Dict[str, Any]] = {} - model_dependencies: Optional[Dict[str, str]] + model_dependencies: Optional[Dict[str, str]] = {} - @pydantic.validator("model_dependencies", always=True, pre=True) + model_config = pydantic.ConfigDict(protected_namespaces=("settings",)) + + @pydantic.field_validator("model_dependencies", mode="before") + @classmethod def validate_dependencies(cls, v): - if not v: + if v is None: return {} if isinstance(v, (list, set)): return {key: key for key in v} @@ -55,7 +59,7 @@ def walk_objects(mod): def _configurations_from_objects(m) -> Dict[str, ModelConfiguration]: if inspect.isclass(m) and issubclass(m, Asset): return { - key: ModelConfiguration(**{**config, "model_type": m}) + key: ModelConfiguration(**config, model_type=m) for key, config in m.CONFIGURATIONS.items() } elif isinstance(m, (list, tuple)): @@ -92,7 +96,9 @@ def configure( if isinstance(conf_value, ModelConfiguration): conf[key] = conf_value elif isinstance(conf_value, dict): - conf[key] = ModelConfiguration(**{**conf[key].dict(), **conf_value}) + conf[key] = ModelConfiguration( + **{**conf[key].model_dump(), **conf_value} + ) for key in set(configuration.keys()) - set(conf.keys()): conf_value = configuration[key] if isinstance(conf_value, ModelConfiguration): diff --git a/modelkit/core/models/distant_model.py b/modelkit/core/models/distant_model.py index b15ce459..cf42f78e 100644 --- a/modelkit/core/models/distant_model.py +++ b/modelkit/core/models/distant_model.py @@ -74,7 +74,7 @@ async def _predict(self, item, **kwargs): except TypeError: # TypeError: Object of type {ItemType} is not JSON serializable # Try converting the pydantic model to json directly - item = item.json() + item = item.model_dump_json() async with self.aiohttp_session.post( self.endpoint, params=kwargs.get("endpoint_params", self.endpoint_params), @@ -123,7 +123,7 @@ def _predict(self, item, **kwargs): except TypeError: # TypeError: Object of type {ItemType} is not JSON serializable # Try converting the pydantic model to json directly - item = item.json() + item = item.model_dump_json() response = self.requests_session.post( self.endpoint, params=kwargs.get("endpoint_params", self.endpoint_params), @@ -177,7 +177,7 @@ def _predict_batch(self, items, **kwargs): except TypeError: # TypeError: Object of type {ItemType} is not JSON serializable # Try converting a list of pydantic models to dict - items = json.dumps([item.dict() for item in items]) + items = json.dumps([item.model_dump() for item in items]) response = self.requests_session.post( self.endpoint, params=kwargs.get("endpoint_params", self.endpoint_params), @@ -228,7 +228,7 @@ async def _predict_batch(self, items, **kwargs): except TypeError: # TypeError: Object of type {ItemType} is not JSON serializable # Try converting a list of pydantic models to dict - items = json.dumps([item.dict() for item in items]) + items = json.dumps([item.model_dump() for item in items]) async with self.aiohttp_session.post( self.endpoint, diff --git a/modelkit/core/settings.py b/modelkit/core/settings.py index 6ee67467..c996cc36 100644 --- a/modelkit/core/settings.py +++ b/modelkit/core/settings.py @@ -1,15 +1,59 @@ from typing import Optional, Union import pydantic +import pydantic_settings +from typing_extensions import Annotated + + +class ModelkitSettings(pydantic_settings.BaseSettings): + """ + Custom pydantic settings needed to allow setting both: + - the validation alias + - the field name + And to prioritize the field name over the environment variable name. + It aims at replacing the deprecated, pydantic 1.*, `env` argument to `Field`. + It requires the use of the `validation_alias` and AliasChoices arguments in + the child class. + + Example: + class ServingSettings(ModelkitSettings): + enable: bool = pydantic.Field( + False, + validation_alias=pydantic.AliasChoices( + "enable", + "MODELKIT_TF_SERVING_ENABLE", + ), + ) + assert ServingSettings().enable is False + assert ServingSettings(enable=True).enable is True + + os.environ["MODELKIT_TF_SERVING_ENABLE"] = "True" + assert ServingSettings().enable is True + assert ServingSettings(enable=False).enable is False + + """ + + model_config = pydantic.ConfigDict(extra="ignore") + + +class TFServingSettings(ModelkitSettings): + enable: bool = pydantic.Field( + False, + validation_alias=pydantic.AliasChoices("enable", "MODELKIT_TF_SERVING_ENABLE"), + ) + mode: str = pydantic.Field( + "rest", + validation_alias=pydantic.AliasChoices("mode", "MODELKIT_TF_SERVING_MODE"), + ) + host: str = pydantic.Field( + "localhost", + validation_alias=pydantic.AliasChoices("host", "MODELKIT_TF_SERVING_HOST"), + ) + port: int = pydantic.Field( + 8501, validation_alias=pydantic.AliasChoices("port", "MODELKIT_TF_SERVING_PORT") + ) - -class TFServingSettings(pydantic.BaseSettings): - enable: bool = pydantic.Field(False, env="MODELKIT_TF_SERVING_ENABLE") - mode: str = pydantic.Field("rest", env="MODELKIT_TF_SERVING_MODE") - host: str = pydantic.Field("localhost", env="MODELKIT_TF_SERVING_HOST") - port: int = pydantic.Field(8501, env="MODELKIT_TF_SERVING_PORT") - - @pydantic.validator("port") + @pydantic.field_validator("port") @classmethod def default_serving_port(cls, v, values): if not v: @@ -17,30 +61,36 @@ def default_serving_port(cls, v, values): return v -class CacheSettings(pydantic.BaseSettings): - cache_provider: Optional[str] = pydantic.Field(None, env="MODELKIT_CACHE_PROVIDER") +class CacheSettings(ModelkitSettings): + cache_provider: Optional[str] = pydantic.Field( + None, + validation_alias=pydantic.AliasChoices( + "cache_provider", "MODELKIT_CACHE_PROVIDER" + ), + ) class RedisSettings(CacheSettings): - host: str = pydantic.Field("localhost", env="MODELKIT_CACHE_HOST") - port: int = pydantic.Field(6379, env="MODELKIT_CACHE_PORT") - - @pydantic.validator("cache_provider") - def _validate_type(cls, v): - if v != "redis": - raise ValueError - return v + host: str = pydantic.Field( + "localhost", + validation_alias=pydantic.AliasChoices("host", "MODELKIT_CACHE_HOST"), + ) + port: int = pydantic.Field( + 6379, validation_alias=pydantic.AliasChoices("port", "MODELKIT_CACHE_PORT") + ) class NativeCacheSettings(CacheSettings): - implementation: str = pydantic.Field("LRU", env="MODELKIT_CACHE_IMPLEMENTATION") - maxsize: int = pydantic.Field(128, env="MODELKIT_CACHE_MAX_SIZE") - - @pydantic.validator("cache_provider") - def _validate_type(cls, v): - if v != "native": - raise ValueError - return v + implementation: str = pydantic.Field( + "LRU", + validation_alias=pydantic.AliasChoices( + "implementation", "MODELKIT_CACHE_IMPLEMENTATION" + ), + ) + maxsize: int = pydantic.Field( + 128, + validation_alias=pydantic.AliasChoices("maxsize", "MODELKIT_CACHE_MAX_SIZE"), + ) def cache_settings(): @@ -57,19 +107,35 @@ def cache_settings(): pass -class LibrarySettings(pydantic.BaseSettings): - lazy_loading: bool = pydantic.Field(False, env="MODELKIT_LAZY_LOADING") - override_assets_dir: Optional[str] = pydantic.Field( - None, env="MODELKIT_ASSETS_DIR_OVERRIDE" +def _get_library_settings_cache_provider(v: Optional[str]) -> str: + if v is None: + return "none" + elif isinstance(v, dict): + return v.get("cache_provider", "none") + return getattr(v, "cache_provider", "none") + + +class LibrarySettings(ModelkitSettings): + lazy_loading: bool = pydantic.Field( + False, + validation_alias=pydantic.AliasChoices("lazy_loading", "MODELKIT_LAZY_LOADING"), ) - enable_validation: bool = pydantic.Field(True, env="MODELKIT_ENABLE_VALIDATION") - tf_serving: TFServingSettings = pydantic.Field( - default_factory=lambda: TFServingSettings() + override_assets_dir: Optional[str] = pydantic.Field( + None, + validation_alias=pydantic.AliasChoices( + "override_assets_dir", "MODELKIT_ASSETS_DIR_OVERRIDE" + ), ) - cache: Optional[Union[RedisSettings, NativeCacheSettings]] = pydantic.Field( - default_factory=lambda: cache_settings() + tf_serving: TFServingSettings = pydantic.Field(default_factory=TFServingSettings) + cache: Annotated[ + Union[ + Annotated[RedisSettings, pydantic.Tag("redis")], + Annotated[NativeCacheSettings, pydantic.Tag("native")], + Annotated[None, pydantic.Tag("none")], + ], + pydantic.Discriminator(_get_library_settings_cache_provider), + ] = pydantic.Field( + default_factory=cache_settings, + union_mode="left_to_right", ) - - class Config: - env_prefix = "" - extra = "allow" + model_config = pydantic.ConfigDict(extra="allow") diff --git a/modelkit/core/types.py b/modelkit/core/types.py index ebf75b96..47959751 100644 --- a/modelkit/core/types.py +++ b/modelkit/core/types.py @@ -17,7 +17,7 @@ LibraryModelsType = Union[ModuleType, Type, List, str] -class TestCase(pydantic.generics.GenericModel, Generic[TestItemType, TestReturnType]): +class TestCase(pydantic.BaseModel, Generic[TestItemType, TestReturnType]): item: TestItemType result: TestReturnType keyword_args: Dict[str, Any] = {} diff --git a/modelkit/testing/fixtures.py b/modelkit/testing/fixtures.py index 3f140525..fcf4a6d4 100644 --- a/modelkit/testing/fixtures.py +++ b/modelkit/testing/fixtures.py @@ -27,7 +27,7 @@ def modellibrary_auto_test( configuration=None, models=None, required_models=None, - #  fixture name + # fixture name fixture_name="testing_model_library", test_name="testing_model_library", necessary_fixtures=None, @@ -60,11 +60,13 @@ def test_function(model_key, item, result, kwargs, request): if isinstance(result, JSONTestResult): ref = ReferenceJson(os.path.join(test_dir, os.path.dirname(result.fn))) if isinstance(pred, pydantic.BaseModel): - pred = pred.dict() + pred = pred.model_dump() ref.assert_equal(os.path.basename(result.fn), pred) elif has_numpy and isinstance(result, np.ndarray): assert np.array_equal(pred, result), f"{pred} != {result}" else: + if isinstance(pred, pydantic.BaseModel) and isinstance(result, dict): + pred = pred.model_dump() assert pred == result, f"{pred} != {result}" # in order for the above functions to be collected by pytest, add them @@ -80,14 +82,14 @@ def modellibrary_fixture( configuration=None, models=None, required_models=None, - #  fixture name + # fixture name fixture_name="testing_model_library", necessary_fixtures=None, fixture_scope="session", ): import pytest - #  create a named fixture with the ModelLibrary + # create a named fixture with the ModelLibrary @pytest.fixture(name=fixture_name, scope=fixture_scope) def fixture_function(request): if necessary_fixtures: diff --git a/modelkit/utils/cache.py b/modelkit/utils/cache.py index 2441ecf9..0c6044bb 100644 --- a/modelkit/utils/cache.py +++ b/modelkit/utils/cache.py @@ -61,7 +61,7 @@ def get(self, model_key: str, item: Any, kwargs: Dict[str, Any]): def set(self, k: bytes, d: Any): if isinstance(d, pydantic.BaseModel): - self.redis.set(k, pickle.dumps(d.dict())) + self.redis.set(k, pickle.dumps(d.model_dump())) else: self.redis.set(k, pickle.dumps(d)) diff --git a/modelkit/utils/pretty.py b/modelkit/utils/pretty.py index c5bc8f41..e2fa2d53 100644 --- a/modelkit/utils/pretty.py +++ b/modelkit/utils/pretty.py @@ -13,11 +13,12 @@ def describe(obj, t=None): if not t: t = Tree("") - if hasattr(obj, "__fields__"): - for field_name, field in obj.__fields__.items(): + if hasattr(obj, "model_fields"): + for field_name, field in obj.model_fields.items(): + field_type = field.annotation sub_t = t.add( f"[deep_sky_blue1]{field_name}[/deep_sky_blue1] [dim]: " - f"{pretty_print_type(field.outer_type_)}[/dim]" + f"{pretty_print_type(field_type)}[/dim]" ) describe(getattr(obj, field_name), t=sub_t) elif isinstance(obj, dict): diff --git a/modelkit/utils/pydantic.py b/modelkit/utils/pydantic.py deleted file mode 100644 index 3b0c5cd8..00000000 --- a/modelkit/utils/pydantic.py +++ /dev/null @@ -1,32 +0,0 @@ -def construct_recursive(cls, _fields_set=None, **values): - # https://github.com/samuelcolvin/pydantic/issues/1168 - m = cls.__new__(cls) - fields_values = {} - - for name, field in cls.__fields__.items(): - key = field.alias - if key in values: # this check is necessary or Optional fields will crash - try: - # if issubclass(field.type_, BaseModel): # this is cleaner but slower - if field.shape == 2: - fields_values[name] = [ - construct_recursive(field.type_, **e) for e in values[key] - ] - else: - fields_values[name] = construct_recursive( - field.outer_type_, **values[key] - ) - except (AttributeError, TypeError): - if values[key] is None and not field.required: - fields_values[name] = field.get_default() - else: - fields_values[name] = values[key] - elif not field.required: - fields_values[name] = field.get_default() - - object.__setattr__(m, "__dict__", fields_values) - if _fields_set is None: - _fields_set = set(values.keys()) - object.__setattr__(m, "__fields_set__", _fields_set) - m._init_private_attributes() - return m diff --git a/pyproject.toml b/pyproject.toml index ad1f45ba..920fdb24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,8 @@ dependencies = [ "click", "filelock", "humanize", - "pydantic<2.0", + "pydantic>=2.0", + "pydantic-settings", "python-dateutil", "redis", "requests", @@ -92,7 +93,7 @@ dev = [ "pytest-timeout", "nox", # releases - "bump-my-version<0.11.0", # remove upper bound once migrated to pydantic 2 + "bump-my-version", # remove upper bound once migrated to pydantic 2 # docs "mkdocs-material", "pymdown-extensions>=10.0", # resolve CVE-2023-32309 diff --git a/requirements-dev.txt b/requirements-dev.txt index a0e611c9..582993f4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,6 +9,8 @@ aiohttp==3.9.1 # via modelkit (pyproject.toml) aiosignal==1.3.1 # via aiohttp +annotated-types==0.6.0 + # via pydantic anyio==4.1.0 # via httpx argcomplete==3.1.6 @@ -23,7 +25,7 @@ babel==2.13.1 # via mkdocs-material build==1.0.3 # via pip-tools -bump-my-version==0.10.0 +bump-my-version==0.12.0 # via modelkit (pyproject.toml) cachetools==5.3.2 # via modelkit (pyproject.toml) @@ -144,7 +146,14 @@ pre-commit==3.5.0 # via modelkit (pyproject.toml) pycparser==2.21 # via cffi -pydantic==1.10.13 +pydantic==2.5.2 + # via + # bump-my-version + # modelkit (pyproject.toml) + # pydantic-settings +pydantic-core==2.14.5 + # via pydantic +pydantic-settings==2.1.0 # via # bump-my-version # modelkit (pyproject.toml) @@ -171,6 +180,8 @@ python-dateutil==2.8.2 # via # ghp-import # modelkit (pyproject.toml) +python-dotenv==1.0.0 + # via pydantic-settings pyyaml==6.0.1 # via # mkdocs @@ -226,6 +237,7 @@ typing-extensions==4.8.0 # modelkit (pyproject.toml) # mypy # pydantic + # pydantic-core # rich-click urllib3==2.1.0 # via diff --git a/tests/conftest.py b/tests/conftest.py index b459e2d4..35ec068f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,6 @@ def clean_env(): "MODELKIT_CACHE_PORT", "MODELKIT_CACHE_PROVIDER", "MODELKIT_DEFAULT_PACKAGE", - "MODELKIT_ENABLE_VALIDATION", "MODELKIT_LAZY_LOADING", "MODELKIT_STORAGE_BUCKET", "MODELKIT_STORAGE_FORCE_DOWNLOAD", diff --git a/tests/test_caching.py b/tests/test_caching.py index d4d445b1..454ed8b7 100644 --- a/tests/test_caching.py +++ b/tests/test_caching.py @@ -101,13 +101,19 @@ def finalize(): def _do_model_test(model, ITEMS): for i in ITEMS: res = model(i, _force_compute=True) + if isinstance(res, pydantic.BaseModel): + res = res.model_dump() assert i == res - assert model.predict_batch(ITEMS) == ITEMS + batch_results = model.predict_batch(ITEMS) + if isinstance(batch_results[0], pydantic.BaseModel): + batch_results = [res.model_dump() for res in batch_results] + assert batch_results == ITEMS - assert ITEMS + [{"ok": {"boomer": [-1]}}] == model.predict_batch( - ITEMS + [{"ok": {"boomer": [-1]}}] - ) + batch_results = model.predict_batch(ITEMS + [{"ok": {"boomer": [-1]}}]) + if isinstance(batch_results[0], pydantic.BaseModel): + batch_results = [res.model_dump() for res in batch_results] + assert batch_results == ITEMS + [{"ok": {"boomer": [-1]}}] @skip_unless("ENABLE_REDIS_TEST", "True") @@ -162,12 +168,18 @@ def _predict_batch(self, items): async def _do_model_test_async(model, ITEMS): for i in ITEMS: res = await model(i, _force_compute=True) + if isinstance(res, pydantic.BaseModel): + res = res.model_dump() assert i == res res = await model.predict_batch(ITEMS) + if isinstance(res[0], pydantic.BaseModel): + res = [item.model_dump() for item in res] assert res == ITEMS res = await model.predict_batch(ITEMS + [{"ok": {"boomer": [-1]}}]) + if isinstance(res[0], pydantic.BaseModel): + res = [item.model_dump() for item in res] assert ITEMS + [{"ok": {"boomer": [-1]}}] == res diff --git a/tests/test_describe.py b/tests/test_describe.py index e92d8235..4107be5f 100644 --- a/tests/test_describe.py +++ b/tests/test_describe.py @@ -101,24 +101,28 @@ def _predict(self, item): library = ModelLibrary( models=[SomeSimpleValidatedModelA, SomeComplexValidatedModelA] ) - console = Console() + console = Console(no_color=True, force_terminal=False, width=130) with console.capture() as capture: library.describe(console=console) - if platform.system() != "Windows": + if platform.system() == "Windows" or platform.python_version().split(".")[:2] != [ + "3", + "11", + ]: # Output is different on Windows platforms since # modelkit.utils.memory cannot track memory increment # and write it - r = ReferenceText(os.path.join(TEST_DIR, "testdata")) - captured = capture.get() - EXCLUDED = ["load time", "load memory", "asset", "category/asset", os.path.sep] - captured = "\n".join( - line - for line in captured.split("\n") - if not any(x in line for x in EXCLUDED) - ) - r.assert_equal("library_describe.txt", captured) + # It also has a few minor typing differences depending on + # the python version + return + r = ReferenceText(os.path.join(TEST_DIR, "testdata")) + captured = capture.get() + EXCLUDED = ["load time", "load memory", "asset", "category/asset", os.path.sep] + captured = "\n".join( + line for line in captured.split("\n") if not any(x in line for x in EXCLUDED) + ) + r.assert_equal("library_describe.txt", captured) class SomeObject: @@ -127,6 +131,10 @@ def __init__(self) -> None: self.y = 2 +class SomePydanticModel(pydantic.BaseModel): + ... + + @pytest.mark.parametrize( "value", [ @@ -138,7 +146,7 @@ def __init__(self) -> None: [1, 2, 3], [1, 2, 3, [4]], object(), - pydantic.BaseModel(), + SomePydanticModel(), int, SomeObject(), float, @@ -195,7 +203,8 @@ class join_dep(Model[str, str]): def _predict(self, item): return item - console = Console() + console = Console(no_color=True, force_terminal=False, width=130) + library = ModelLibrary(models=[top, right, left, join_dep, right_dep]) for m in ["top", "right", "left", "join_dep", "right_dep"]: library.get(m)._load_time = 0.1 @@ -221,10 +230,15 @@ def _predict(self, item): add_dependencies_load_info(load_info_join_dep, library.get("join_dep")) assert load_info_join_dep == {} - if platform.system() == "Windows": + if platform.system() == "Windows" or platform.python_version().split(".")[:2] != [ + "3", + "11", + ]: # Output is different on Windows platforms since # modelkit.utils.memory cannot track memory increment # and write it + # It also has a few minor typing differences depending on + # the python version return with console.capture() as capture: console.print("join_dep describe:") diff --git a/tests/test_model_serialization.py b/tests/test_model_serialization.py index 74b50929..305da72b 100644 --- a/tests/test_model_serialization.py +++ b/tests/test_model_serialization.py @@ -15,7 +15,7 @@ class ReturnType(pydantic.BaseModel): class SomeModel(Model[ItemType, ReturnType]): def _predict(self, item): - return item + return {"x": item.x} def test_model_serialization(): diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 00000000..56935410 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,24 @@ +import pydantic + +from modelkit.core.settings import ModelkitSettings + + +def test_modelkit_settings_working(monkeypatch): + class ServingSettings(ModelkitSettings): + enable: bool = pydantic.Field( + False, + validation_alias=pydantic.AliasChoices( + "enable", + "SERVING_ENABLE", + ), + ) + + assert ServingSettings().enable is False + assert ServingSettings(enable=True).enable is True + + monkeypatch.setenv("SERVING_ENABLE", "True") + assert ServingSettings().enable is True + # without ModelkitSettings, the following would raise a ValidationError + # because both `enable` and `SERVING_ENABLE` are set and passed to the + # constructor. + assert ServingSettings(enable=False).enable is False diff --git a/tests/test_validate.py b/tests/test_validate.py index fc2befd5..55e6b19a 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -10,17 +10,11 @@ ) from modelkit.core.model import AsyncModel, Model from modelkit.core.settings import LibrarySettings -from modelkit.utils.pydantic import construct_recursive -@pytest.mark.parametrize( - "service_settings", - [ - LibrarySettings(), - LibrarySettings(enable_validation=False), - ], -) -def test_validate_item_spec_pydantic(service_settings): +def test_validate_item_spec_pydantic(): + service_settings = LibrarySettings() + class ItemModel(pydantic.BaseModel): x: int @@ -31,29 +25,22 @@ def _predict(self, item): valid_test_item = {"x": 10} m = SomeValidatedModel(service_settings=service_settings) - assert m(valid_test_item) == valid_test_item + assert m(valid_test_item).model_dump() == valid_test_item - if service_settings.enable_validation: - with pytest.raises(ItemValidationException): - m({"ok": 1}) - with pytest.raises(ItemValidationException): - m({"x": "something", "blabli": 10}) - else: + with pytest.raises(ItemValidationException): m({"ok": 1}) + with pytest.raises(ItemValidationException): m({"x": "something", "blabli": 10}) - assert m.predict_batch([valid_test_item] * 2) == [valid_test_item] * 2 + assert [x.model_dump() for x in m.predict_batch([valid_test_item] * 2)] == [ + valid_test_item + ] * 2 @pytest.mark.asyncio -@pytest.mark.parametrize( - "service_settings", - [ - LibrarySettings(), - LibrarySettings(enable_validation=False), - ], -) -async def test_validate_item_spec_pydantic_async(service_settings): +async def test_validate_item_spec_pydantic_async(): + service_settings = LibrarySettings() + class ItemModel(pydantic.BaseModel): x: int @@ -61,33 +48,24 @@ class AsyncSomeValidatedModel(AsyncModel[ItemModel, Any]): async def _predict(self, item): return item - valid_test_item = {"x": 10} + valid_test_item = ItemModel(x=10) m = AsyncSomeValidatedModel(service_settings=service_settings) res = await m(valid_test_item) assert res == valid_test_item - if service_settings.enable_validation: - with pytest.raises(ItemValidationException): - await m({"ok": 1}) - with pytest.raises(ItemValidationException): - await m({"x": "something", "blabli": 10}) - else: + with pytest.raises(ItemValidationException): await m({"ok": 1}) + with pytest.raises(ItemValidationException): await m({"x": "something", "blabli": 10}) res_list = await m.predict_batch([valid_test_item] * 2) assert res_list == [valid_test_item] * 2 -@pytest.mark.parametrize( - "service_settings", - [ - LibrarySettings(), - LibrarySettings(enable_validation=False), - ], -) -def test_validate_item_spec_pydantic_default(service_settings): +def test_validate_item_spec_pydantic_default(): + service_settings = LibrarySettings() + class ItemType(pydantic.BaseModel): x: int y: str = "ok" @@ -108,22 +86,13 @@ def _predict(self, item, **kwargs): assert res.result == 12 assert res.something_else == "ok" - if service_settings.enable_validation: - with pytest.raises(ItemValidationException): - m({}) - else: - with pytest.raises(AttributeError): - m({}) + with pytest.raises(ItemValidationException): + m({}) -@pytest.mark.parametrize( - "service_settings", - [ - LibrarySettings(), - LibrarySettings(enable_validation=False), - ], -) -def test_validate_item_spec_typing(service_settings): +def test_validate_item_spec_typing(): + service_settings = LibrarySettings() + class SomeValidatedModel(Model[Dict[str, int], Any]): def _predict(self, item): return item @@ -133,31 +102,21 @@ def _predict(self, item): m = SomeValidatedModel(service_settings=service_settings) assert m(valid_test_item) == valid_test_item - if service_settings.enable_validation: - with pytest.raises(ItemValidationException): - m.predict_batch(["ok"]) - - with pytest.raises(ItemValidationException): - m("x") - - with pytest.raises(ItemValidationException): - m.predict_batch([1, 2, 1]) - else: + with pytest.raises(ItemValidationException): m.predict_batch(["ok"]) + + with pytest.raises(ItemValidationException): m("x") + + with pytest.raises(ItemValidationException): m.predict_batch([1, 2, 1]) assert m.predict_batch([valid_test_item] * 2) == [valid_test_item] * 2 -@pytest.mark.parametrize( - "service_settings", - [ - LibrarySettings(), - LibrarySettings(enable_validation=False), - ], -) -def test_validate_return_spec(service_settings): +def test_validate_return_spec(): + service_settings = LibrarySettings() + class ItemModel(pydantic.BaseModel): x: int @@ -169,21 +128,13 @@ def _predict(self, item): ret = m({"x": 10}) assert ret.x == 10 - if m.service_settings.enable_validation: - with pytest.raises(ReturnValueValidationException): - m({"x": "something", "blabli": 10}) - else: - m.predict({"x": "something", "blabli": 10}) + with pytest.raises(ReturnValueValidationException): + m({"x": "something", "blabli": 10}) -@pytest.mark.parametrize( - "service_settings", - [ - LibrarySettings(), - LibrarySettings(enable_validation=False), - ], -) -def test_validate_list_items(service_settings): +def test_validate_list_items(): + service_settings = LibrarySettings() + class ItemModel(pydantic.BaseModel): x: str y: str = "ok" @@ -198,20 +149,15 @@ def _predict(self, item): return item m = SomeValidatedModel(service_settings=service_settings) - m.predict_batch([{"x": 10, "y": "ko"}] * 10) + m.predict_batch([{"x": "10", "y": "ko"}] * 10) assert m.counter == 10 - m({"x": 10, "y": "ko"}) + m({"x": "10", "y": "ko"}) assert m.counter == 11 -@pytest.mark.parametrize( - "service_settings", - [ - LibrarySettings(), - LibrarySettings(enable_validation=False), - ], -) -def test_validate_none(service_settings): +def test_validate_none(): + service_settings = LibrarySettings() + class SomeValidatedModel(Model): def _predict(self, item): return item @@ -221,34 +167,6 @@ def _predict(self, item): assert m(1) == 1 -def test_construct_recursive(): - class Item(pydantic.BaseModel): - class SubItem(pydantic.BaseModel): - class SubSubItem(pydantic.BaseModel): - a: str - - z: SubSubItem - - class ListItem(pydantic.BaseModel): - content: int - - x: int - y: SubItem - d: Dict[str, int] - z: List[ListItem] - - item_data = { - "x": 1, - "y": {"z": {"a": "ok"}}, - "d": {"ok": 1}, - "z": [{"content": "content"}], - } - - item_construct_recursive = construct_recursive(Item, **item_data) - assert item_construct_recursive.y.z.a == "ok" - assert item_construct_recursive.z[0].content == "content" - - def test_pydantic_error_truncation(): class ListModel(pydantic.BaseModel): values: List[int] @@ -257,7 +175,7 @@ class ListModel(pydantic.BaseModel): with pytest.raises(ModelkitDataValidationException): try: ListModel(values=["ok"] * 100) - except pydantic.error_wrappers.ValidationError as exc: + except pydantic.ValidationError as exc: raise ModelkitDataValidationException( "test error", pydantic_exc=exc ) from exc @@ -266,7 +184,7 @@ class ListModel(pydantic.BaseModel): with pytest.raises(ModelkitDataValidationException): try: ListModel(values=["ok"]) - except pydantic.error_wrappers.ValidationError as exc: + except pydantic.ValidationError as exc: raise ModelkitDataValidationException( "test error", pydantic_exc=exc ) from exc diff --git a/tests/testdata/library_describe.txt b/tests/testdata/library_describe.txt index 289d9c07..938cd275 100644 --- a/tests/testdata/library_describe.txt +++ b/tests/testdata/library_describe.txt @@ -1,45 +1,37 @@ Settings ├── lazy_loading : bool = False -├── enable_validation : bool = True ├── tf_serving : modelkit.core.settings.TFServingSettings │ ├── enable : bool = False │ ├── mode : str = 'rest' │ ├── host : str = 'localhost' │ └── port : int = 8501 -└── cache : typing.Union[modelkit.core.settings.RedisSettings, - modelkit.core.settings.NativeCacheSettings, NoneType] = None +└── cache : redis = None Configuration ├── some_complex_model_a : ModelConfiguration -│ ├── model_type : typing.Type[modelkit.core.model.Asset] = -│ │ SomeComplexValidatedModelA type -│ ├── model_settings : typing.Dict[str, typing.Any] +│ ├── model_type : typing.Type[modelkit.core.model.Asset] = SomeComplexValidatedModelA type +│ ├── model_settings : typing.Optional[typing.Dict[str, typing.Any]] │ │ └── batch_size : int = 128 -│ └── model_dependencies : typing.Dict[str, str] +│ └── model_dependencies : typing.Optional[typing.Dict[str, str]] │ └── some_model_a : str = 'some_model_a' └── some_model_a : ModelConfiguration - ├── model_type : typing.Type[modelkit.core.model.Asset] = - │ SomeSimpleValidatedModelA type - ├── model_settings : typing.Dict[str, typing.Any] = {} - └── model_dependencies : typing.Dict[str, str] = {} + ├── model_type : typing.Type[modelkit.core.model.Asset] = SomeSimpleValidatedModelA type + ├── model_settings : typing.Optional[typing.Dict[str, typing.Any]] = {} + └── model_dependencies : typing.Optional[typing.Dict[str, str]] = {} Assets - ├── path : str = - └── version : str = None + └── version : typing.Optional[str] = None Models -├── some_model_a : SomeSimpleValidatedModelA = SomeSimpleValidatedModelA -│ instance +├── some_model_a : SomeSimpleValidatedModelA = SomeSimpleValidatedModelA instance │ ├── configuration: some_model_a │ ├── doc: This is a summary │ │ │ │ that also has plenty more text │ ├── signature: str -> str -└── some_complex_model_a : SomeComplexValidatedModelA = - SomeComplexValidatedModelA instance +└── some_complex_model_a : SomeComplexValidatedModelA = SomeComplexValidatedModelA instance ├── configuration: some_complex_model_a ├── doc: More complex │ │ With **a lot** of documentation - ├── signature: tests.test_describe.test_describe..ItemModel -> - │ tests.test_describe.test_describe..ResultModel + ├── signature: tests.test_describe.test_describe..ItemModel -> tests.test_describe.test_describe..ResultModel ├── batch size: 128 ├── model settings │ └── batch_size : int = 128 From b9895293c60bcf3b3f805b0bc6815520de24b764 Mon Sep 17 00:00:00 2001 From: Antoine Jeannot Date: Tue, 28 Nov 2023 10:01:18 +0100 Subject: [PATCH 3/5] migration: add docs --- README.md | 21 +++++++++++++--- docs/migration.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 docs/migration.md diff --git a/README.md b/README.md index 12915ddf..9f8cabb8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

Python framework for production ML systems.

- + ---

@@ -21,7 +21,7 @@

-`modelkit` is a minimalist yet powerful MLOps library for Python, built for people who want to deploy ML models to production. +`modelkit` is a minimalist yet powerful MLOps library for Python, built for people who want to deploy ML models to production. It packs several features which make your go-to-production journey a breeze, and ensures that the same exact code will run in production, on your machine, or on data processing pipelines. @@ -64,7 +64,7 @@ In addition, you will find that `modelkit` is: ## Installation -Install with `pip`: +Install the latest stable release with `pip`: ``` pip install modelkit @@ -72,6 +72,19 @@ pip install modelkit Optional dependencies are available for remote storage providers ([see documentation](https://cornerstone-ondemand.github.io/modelkit/assets/storage_provider/#using-different-providers)) +### 🚧 Beta release + +`modelkit 0.1` and onwards will be shipped with `pydantic 2`, bringing significant performance improvements 🎉 ⚡ + +To try out the beta before it is stable: + +``` +pip install --pre modelkit +``` + +Also, you can refer to the [modelkit migration note](https://cornerstone-ondemand.github.io/modelkit/migration.md) + to ease the migration process! + ## Community Join our [community](https://discord.gg/ayj5wdAArV) on Discord to get support and leave feedback @@ -83,6 +96,6 @@ Contributors, if you want to install and test locally: # install make setup -# lint & test +# lint & test make tests ``` diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 00000000..3f79d1d7 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,64 @@ +# Modelkit 0.1 migration note + +Modelkit relies on `pydantic` as part of its validation process. + +Modelkit 0.1 and onwards will be shipped with `pydantic 2`, which comes with __significant__ performance improvements at the cost of breaking changes. + +Details on how to migrate to `pydantic 2` are available in the corresponding migration guide: https://docs.pydantic.dev/latest/migration/ + +### Installation +To install and try out the `modelkit 0.1.0.bX` beta before its stable release: +``` +pip install --pre modelkit +``` + +### Known breaking changes + +Some breaking changes are arising while upgrading to `pydantic 2` and the new `modelkit 0.1 beta`. Here is a brief, rather exhaustive, list of the encountered issues or dropped features. + +#### Drop: implicit pydantic model conversion + +With `pydantic < 2` and `modelkit < 0.1`, the following pattern was authorized (even though not advised) due to implicit conversions between pydantic models: + +```python +import modelkit +import pydantic +import typing + +class OutputItem(pydantic.BaseModel): + x: int + +class AnotherOutputItem(pydantic.BaseModel): + x: int + +class MyModel(modelkit.Model[int, OutputItem]): + def _predict(self, item): + return AnotherOutputItem(x=item) + +model = MyModel() +model(1) # raises! + +``` + +__This pattern is no longer allowed__. + +However, here are the fixes: +- directly build the right output `pydantic` Model (here: `OutputItem`) +- directly use dicts to benefit from the dict to model conversion from `pydantic` and `modelkit` (or via `.model_dump()`) + +### Drop: model validation deactivation + +The `MODELKIT_ENABLE_VALIDATION` environment variable (or the `enable_validation` parameter of the `LibrarySettings`) which allowed one to deactivate validation if set to `False` was removed. + +This feature has worked for `pydantic < 2` for rather simple `pydantic models` but not complex ones with nested structures (see: https://github.com/Cornerstone-OnDemand/modelkit/pull/8). However, it still is an open question in `pydantic 2`, whether to allow recursive construction of models without validation (see: https://github.com/pydantic/pydantic/issues/8084). +Due to the fact `pydantic 2` brings heavy performance improvements, this feature has not been re-implemented. + +Fixes: None, just prepare to have your inputs / outputs validated :) + +### Development Workflows + +The beta release, along with subsequent patches, will be pushed to the main branch. Prior to the stable release, tags will adopt the format `0.1.0.bX` + +For projects that have not migrated, `modelkit 0.0` will continue to receive maintenance on the `v0.0-maintenance` branch. Releases on PyPI and manual tags will adhere to the usual process. + +To prevent your project from automatically upgrading to the new modelkit 0.1 upon its stable release, you can enforce an upper bound constraint in your requirements, e.g.: `modelkit<0.1` \ No newline at end of file From ffc1c9aeb3bc773e5a34aa27e84b1b3bea708aa6 Mon Sep 17 00:00:00 2001 From: Antoine Jeannot Date: Tue, 28 Nov 2023 21:47:58 +0100 Subject: [PATCH 4/5] test_describe for python >= 3.11 --- tests/test_describe.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/test_describe.py b/tests/test_describe.py index 4107be5f..e3964c25 100644 --- a/tests/test_describe.py +++ b/tests/test_describe.py @@ -1,5 +1,6 @@ import os import platform +import sys from typing import Any import pydantic @@ -106,10 +107,7 @@ def _predict(self, item): with console.capture() as capture: library.describe(console=console) - if platform.system() == "Windows" or platform.python_version().split(".")[:2] != [ - "3", - "11", - ]: + if platform.system() == "Windows" or sys.version_info[:2] < (3, 11): # Output is different on Windows platforms since # modelkit.utils.memory cannot track memory increment # and write it @@ -230,10 +228,7 @@ def _predict(self, item): add_dependencies_load_info(load_info_join_dep, library.get("join_dep")) assert load_info_join_dep == {} - if platform.system() == "Windows" or platform.python_version().split(".")[:2] != [ - "3", - "11", - ]: + if platform.system() == "Windows" or sys.version_info[:2] < (3, 11): # Output is different on Windows platforms since # modelkit.utils.memory cannot track memory increment # and write it From 9857df898d98dacb6de8829c9d1c2020758fefa0 Mon Sep 17 00:00:00 2001 From: Antoine Jeannot Date: Wed, 29 Nov 2023 09:54:52 +0100 Subject: [PATCH 5/5] remove deprecated comment --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 920fdb24..01d2254b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ dev = [ "pytest-timeout", "nox", # releases - "bump-my-version", # remove upper bound once migrated to pydantic 2 + "bump-my-version", # docs "mkdocs-material", "pymdown-extensions>=10.0", # resolve CVE-2023-32309