From 692299c8f8bcaebf764d4ab3e48750273be0d283 Mon Sep 17 00:00:00 2001 From: Timofey Date: Mon, 18 Sep 2023 18:08:01 +0500 Subject: [PATCH] Create python-package.yml with pytests. Fix code by different python versions tests tests --- .github/workflows/python-package.yml | 44 +++++++++++++++++++ mlup/config.py | 2 +- mlup/console_scripts/command.py | 6 +-- mlup/ml/binarization/base.py | 2 +- mlup/ml/binarization/joblib.py | 2 +- mlup/ml/binarization/lightgbm.py | 2 +- mlup/ml/binarization/memory.py | 2 +- mlup/ml/binarization/onnx.py | 2 +- mlup/ml/binarization/pickle.py | 2 +- mlup/ml/binarization/tensorflow.py | 4 +- mlup/ml/binarization/torch.py | 4 +- mlup/ml/model.py | 9 ++-- mlup/up.py | 4 +- mlup/utils/interspection.py | 5 ++- mlup/utils/logging.py | 4 +- mlup/utils/loop.py | 8 ++++ mlup/web/api_validators.py | 2 +- mlup/web/app.py | 4 +- mlup/web/architecture/batching.py | 4 +- mlup/web/architecture/worker_and_queue.py | 3 +- pyproject.toml | 26 +++++++---- tests/conftest.py | 4 +- tests/integration_tests/conftest.py | 2 +- .../frameworks/test_lightgbm_model.py | 3 +- .../frameworks/test_pytorch_model.py | 3 +- .../frameworks/test_scikit_learn_model.py | 3 +- .../frameworks/test_tensorflow_model.py | 3 +- tests/integration_tests/test_serilization.py | 9 ++-- tests/unit_tests/console_scripts/test_run.py | 8 ++-- .../console_scripts/test_validate_config.py | 2 +- tests/unit_tests/ml/test_binarization.py | 9 ++-- tests/unit_tests/ml/test_data_transformers.py | 13 +++--- tests/unit_tests/ml/test_model.py | 10 ++--- tests/unit_tests/ml/test_model_config.py | 2 +- tests/unit_tests/ml/test_storage.py | 25 +++++++---- tests/unit_tests/test_configs.py | 12 ++--- tests/unit_tests/utils/test_interspection.py | 10 ++++- tests/unit_tests/web/test_api_docs.py | 8 +++- tests/unit_tests/web/test_app.py | 8 ++-- tests/unit_tests/web/test_architecture.py | 7 +-- tox.ini | 6 +++ 41 files changed, 194 insertions(+), 94 deletions(-) create mode 100644 .github/workflows/python-package.yml create mode 100644 tox.ini diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..af6ecf4 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,44 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: PyTests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 mypy pytest + if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi + pip install .[tests] + - name: flake8 + run: | + flake8 --count --show-source --statistics mlup tests examples +# - name: mypy +# run: | +# - mypy . + - name: Unit tests + run: | + pytest tests/unit_tests + - name: Integration tests + run: | + pytest tests/integration_tests diff --git a/mlup/config.py b/mlup/config.py index 8113acd..e7dd33d 100644 --- a/mlup/config.py +++ b/mlup/config.py @@ -120,7 +120,7 @@ def get_config_dict(self) -> Dict: return config def load_from_dict(self, conf: Dict): - logger.info(f'Load config from dict') + logger.info('Load config from dict') self.set_config_from_dict(conf) def load_from_json(self, file_path: Union[str, Path]): diff --git a/mlup/console_scripts/command.py b/mlup/console_scripts/command.py index acbe9c3..cb974b9 100644 --- a/mlup/console_scripts/command.py +++ b/mlup/console_scripts/command.py @@ -17,14 +17,14 @@ options: -h, --help show this help message and exit - + {AVAILABLE_COMMANDS} """ def run_command(args: List[str]): try: - command, command_args = args[0], args[1:] + command, _ = args[0], args[1:] except IndexError: print(HELP) sys.exit(1) @@ -37,7 +37,7 @@ def run_command(args: List[str]): try: module = importlib.import_module(f'mlup.console_scripts.{command}') - except ModuleNotFoundError as e: + except ModuleNotFoundError: print(f'Invalid command {command} - mlup.console_scripts.{command}.') print(AVAILABLE_COMMANDS) sys.exit(1) diff --git a/mlup/ml/binarization/base.py b/mlup/ml/binarization/base.py index 784fe9d..618a4e5 100644 --- a/mlup/ml/binarization/base.py +++ b/mlup/ml/binarization/base.py @@ -4,7 +4,7 @@ from mlup.constants import LoadedFile -@dataclass(kw_only=True) +@dataclass class BaseBinarizer(metaclass=abc.ABCMeta): @classmethod @abc.abstractmethod diff --git a/mlup/ml/binarization/joblib.py b/mlup/ml/binarization/joblib.py index c8c8979..5c9ad71 100644 --- a/mlup/ml/binarization/joblib.py +++ b/mlup/ml/binarization/joblib.py @@ -19,7 +19,7 @@ class JoblibBinarizer(PickleBinarizer): @classmethod def deserialize(cls, data: LoadedFile): - logger.info(f'Run deserialization joblib data.') + logger.info('Run deserialization joblib data.') if joblib is None: logger.error('For use joblib, please install it. pip install joblib.') raise ModelBinarizationError('For use joblib, please install it. pip install joblib.') diff --git a/mlup/ml/binarization/lightgbm.py b/mlup/ml/binarization/lightgbm.py index 42cc85e..307ba49 100644 --- a/mlup/ml/binarization/lightgbm.py +++ b/mlup/ml/binarization/lightgbm.py @@ -14,7 +14,7 @@ class LightGBMBinarizer(BaseBinarizer): @classmethod def deserialize(cls, data: LoadedFile): - logger.info(f'Run deserialization lightgbm data.') + logger.info('Run deserialization lightgbm data.') with TimeProfiler('Time to deserialization lightgbm data:'): try: if data.path: diff --git a/mlup/ml/binarization/memory.py b/mlup/ml/binarization/memory.py index 9df4b63..e71517a 100644 --- a/mlup/ml/binarization/memory.py +++ b/mlup/ml/binarization/memory.py @@ -9,5 +9,5 @@ class MemoryBinarizer(BaseBinarizer): @classmethod def deserialize(cls, data: LoadedFile): - logger.info(f'Run deserialization memory data.') + logger.info('Run deserialization memory data.') return data.raw_data diff --git a/mlup/ml/binarization/onnx.py b/mlup/ml/binarization/onnx.py index b98f45d..6d5b1cd 100644 --- a/mlup/ml/binarization/onnx.py +++ b/mlup/ml/binarization/onnx.py @@ -27,7 +27,7 @@ def predict(self, input_data): class InferenceSessionBinarizer(BaseBinarizer): @classmethod def deserialize(cls, data: LoadedFile): - logger.info(f'Run deserialization onnxruntime data.') + logger.info('Run deserialization onnxruntime data.') with TimeProfiler('Time to deserialization onnxruntime data:'): try: _data = data.raw_data diff --git a/mlup/ml/binarization/pickle.py b/mlup/ml/binarization/pickle.py index 262fd06..b1c5443 100644 --- a/mlup/ml/binarization/pickle.py +++ b/mlup/ml/binarization/pickle.py @@ -15,7 +15,7 @@ class PickleBinarizer(BaseBinarizer): @classmethod def deserialize(cls, data: LoadedFile): - logger.info(f'Run deserialization pickle data.') + logger.info('Run deserialization pickle data.') with TimeProfiler('Time to deserialization pickle data:'): try: if isinstance(data.raw_data, BufferedIOBase): diff --git a/mlup/ml/binarization/tensorflow.py b/mlup/ml/binarization/tensorflow.py index 4b76698..87d7303 100644 --- a/mlup/ml/binarization/tensorflow.py +++ b/mlup/ml/binarization/tensorflow.py @@ -16,7 +16,7 @@ class TensorFlowBinarizer(BaseBinarizer): @classmethod def deserialize(cls, data: LoadedFile): - logger.info(f'Run deserialization tensorflow data.') + logger.info('Run deserialization tensorflow data.') with TimeProfiler('Time to deserialization tensorflow data:'): try: _data = data.raw_data @@ -63,7 +63,7 @@ def is_this_type(cls, loaded_file: LoadedFile) -> float: class TensorFlowSavedBinarizer(BaseBinarizer): @classmethod def deserialize(cls, data: LoadedFile): - logger.info(f'Run deserialization tensorflow SavedModel data.') + logger.info('Run deserialization tensorflow SavedModel data.') with TimeProfiler('Time to deserialization tensorflow SavedModel data:'): try: _data = data.raw_data diff --git a/mlup/ml/binarization/torch.py b/mlup/ml/binarization/torch.py index 4290adc..daf79af 100644 --- a/mlup/ml/binarization/torch.py +++ b/mlup/ml/binarization/torch.py @@ -15,7 +15,7 @@ class TorchBinarizer(BaseBinarizer): @classmethod def deserialize(cls, data: LoadedFile): - logger.info(f'Run deserialization torch data.') + logger.info('Run deserialization torch data.') with TimeProfiler('Time to deserialization torch data:'): try: _data = data.raw_data @@ -64,7 +64,7 @@ def is_this_type(cls, loaded_file: LoadedFile) -> float: class TorchJITBinarizer(BaseBinarizer): @classmethod def deserialize(cls, data: LoadedFile): - logger.info(f'Run deserialization torch JIT data.') + logger.info('Run deserialization torch JIT data.') with TimeProfiler('Time to deserialization torch JIT data:'): try: _data = data.raw_data diff --git a/mlup/ml/model.py b/mlup/ml/model.py index d321e97..25e19c6 100644 --- a/mlup/ml/model.py +++ b/mlup/ml/model.py @@ -29,7 +29,7 @@ logger = logging.getLogger('mlup') -@dataclass(kw_only=True) +@dataclass class ModelConfig: """ MlupModel config class. This class have settings for model. @@ -149,7 +149,7 @@ def ml_str(self, need_spaces: bool = False): return '\n'.join(res) -@dataclass(kw_only=True, repr=True) +@dataclass(repr=True) class MLupModel(ModelConfig): """This is main UP model class. Create object UP with your ML model, set your settings and run your app. @@ -274,7 +274,7 @@ def _transform_predicted_data(self, predicted_data: Any): try: processing_data = self._data_transformer_for_predicted.transform_to_json_format(predicted_data) except Exception as e: - logger.exception(f'Fail transform predicted data to response format.') + logger.exception('Fail transform predicted data to response format.') raise PredictTransformDataError(str(e)) finally: logger.debug('Finish transform predicted data to response format.') @@ -302,7 +302,7 @@ async def _predict(self, data_for_predict: Optional[Any] = None, **other_predict *_predict_args ) else: - logger.debug(f'Running sync predict.') + logger.debug('Running sync predict.') with self._lock: result = self._prepare_predict_method(**other_predict_args)(*_predict_args) except Exception as e: @@ -423,4 +423,3 @@ async def predict_from(self, **predict_data): raise except Exception as e: raise PredictError(e) - diff --git a/mlup/up.py b/mlup/up.py index c036d41..1188d7c 100644 --- a/mlup/up.py +++ b/mlup/up.py @@ -24,12 +24,12 @@ def generate_default_config(path_to_file: Optional[str] = None) -> Optional[Dict up.to_yaml(path_to_file) -@dataclass(kw_only=True) +@dataclass class Config(ModelConfig, WebAppConfig): pass -@dataclass(kw_only=True, repr=False) +@dataclass(repr=False) class UP: """This is main UP class. Create object UP with your ML model, set your settings and run your web app. diff --git a/mlup/utils/interspection.py b/mlup/utils/interspection.py index ca0d4c5..ea66b5c 100644 --- a/mlup/utils/interspection.py +++ b/mlup/utils/interspection.py @@ -7,6 +7,7 @@ from mlup.constants import IS_X, THERE_IS_ARGS, DEFAULT_X_ARG_NAME, BinarizationType, LoadedFile from mlup.utils.profiling import TimeProfiler + logger = logging.getLogger('mlup') @@ -107,7 +108,7 @@ def example(a, b = 100, *, c: float = 123): param_data['type'] = types[type(param_obj.default)] if param_name.lower().strip() == 'x' and auto_detect_predict_params: - logger.info(f'Found X param in model params. Set List type') + logger.info('Found X param in model params. Set List type') param_data['type'] = 'List' param_data[IS_X] = True _found_X = True @@ -145,7 +146,7 @@ def auto_search_binarization_type(loaded_file: LoadedFile) -> Optional[Type[Bina :rtype: Optional[Type[BinarizationType]] """ - logger.info(f'Run auto search binarizer.') + logger.info('Run auto search binarizer.') probabilities = [] with TimeProfiler('Time to auto search binarizer:', log_level='info'): for binarizer_path in BinarizationType: diff --git a/mlup/utils/logging.py b/mlup/utils/logging.py index 37e7dec..9176538 100644 --- a/mlup/utils/logging.py +++ b/mlup/utils/logging.py @@ -21,7 +21,9 @@ def __init__(self, fmt: Optional[str] = None, *args, **kwargs): def set_fmt(self, fmt_name: str = 'default'): fmt = self._fmts[fmt_name] self._style = logging.PercentStyle(fmt) - self._style.validate() + # This "if" for python3.7- + if hasattr(self._style, 'validate'): + self._style.validate() self._fmt = self._style._fmt diff --git a/mlup/utils/loop.py b/mlup/utils/loop.py index b70b5c2..ef638ff 100644 --- a/mlup/utils/loop.py +++ b/mlup/utils/loop.py @@ -1,4 +1,5 @@ import asyncio +import sys import threading @@ -29,3 +30,10 @@ def run_async(func, *args, **kwargs): return thread.result else: return asyncio.run(func(*args, **kwargs)) + + +def create_async_task(coro, *, name=None): + task_kwargs = {} + if sys.version_info.minor >= 8: + task_kwargs['name'] = name + return asyncio.create_task(coro, **task_kwargs) diff --git a/mlup/web/api_validators.py b/mlup/web/api_validators.py index 0702127..0541dd0 100644 --- a/mlup/web/api_validators.py +++ b/mlup/web/api_validators.py @@ -1,7 +1,7 @@ import logging from typing import List, Any, Dict, Tuple, Optional, Type -from pydantic import BaseModel, validator, create_model, Field +from pydantic import BaseModel, create_model, Field from mlup.constants import IS_X, DEFAULT_X_ARG_NAME from mlup.ml.model import MLupModel diff --git a/mlup/web/app.py b/mlup/web/app.py index c1d6899..52fc7fe 100644 --- a/mlup/web/app.py +++ b/mlup/web/app.py @@ -71,7 +71,7 @@ async def wrap(*args, response: FastAPIResponse, **kwargs): return wrap -@dataclass(kw_only=True) +@dataclass class WebAppConfig: """ WebAppConfig config class. This class have settings for web app. @@ -227,7 +227,7 @@ def wb_str(self, need_spaces: bool = False): return '\n'.join(res) -@dataclass(kw_only=True, repr=True) +@dataclass(repr=True) class MLupWebApp: """This is main UP web app class. Create object UP with your ML model, set your settings and run your web app. diff --git a/mlup/web/architecture/batching.py b/mlup/web/architecture/batching.py index f444332..1c853e4 100644 --- a/mlup/web/architecture/batching.py +++ b/mlup/web/architecture/batching.py @@ -99,7 +99,7 @@ def _save_predicted_data_from_batch(self, predicted_data: List, error: Optional[ local_result = [] async def _predict(self): - logger.debug(f'Run predict for batch') + logger.debug('Run predict for batch') predicted_data = self.batch_queue error = None try: @@ -112,7 +112,7 @@ async def _predict(self): self._save_predicted_data_from_batch(predicted_data, error) self.batch_queue.clear() self.batch_predict_ids.clear() - logger.debug(f'End predict for batch') + logger.debug('End predict for batch') async def _start_worker(self, sleep_time: float = 0.1): self._running = True diff --git a/mlup/web/architecture/worker_and_queue.py b/mlup/web/architecture/worker_and_queue.py index 8ecb4e7..d5708d0 100644 --- a/mlup/web/architecture/worker_and_queue.py +++ b/mlup/web/architecture/worker_and_queue.py @@ -9,6 +9,7 @@ from mlup.errors import WebAppLoadError, PredictError, PredictWaitResultError from mlup.ml.model import MLupModel from mlup.utils.collections import TTLOrderedDict +from mlup.utils.loop import create_async_task from mlup.web.architecture.base import BaseWebAppArchitecture from mlup.constants import WebAppArchitecture @@ -102,7 +103,7 @@ async def run(self): logger.info('Run model in worker') if self.results_storage is None or self.queries_queue is None: raise WebAppLoadError('Not called .load() in web.load()') - self.worker_process = asyncio.create_task(self._start_worker()) + self.worker_process = create_async_task(self._start_worker()) @property def is_running(self) -> bool: diff --git a/pyproject.toml b/pyproject.toml index b85b55b..01d15b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,13 +55,19 @@ Documentation = "https://github.com/nxexox/pymlup/docs" Repository = "https://github.com/nxexox/pymlup" [project.optional-dependencies] -scikit-learn = ["scikit-learn>=1.2.0,<1.3.0"] +scikit-learn = [ + "scikit-learn>=1.2.0,<1.3.0;python_version>='3.8'", + "scikit-learn;python_version<'3.8'", +] lightgbm = ["lightgbm>=4.0.0,<5.0.0"] tensorflow = ["tensorflow>=2.0.0,<3.0.0"] -torch = ["torch>=2.0.0,<3.0.0"] +torch = [ + "torch>=2.0.0,<3.0.0;python_version>='3.8'", + "torch;python_version<'3.8'", +] onnx = [ "onnx>=1.0.0,<2.0.0", - "onnxruntime>=1.0.0,<2.0.0", + "onnxruntime<1.16", ] tests = [ # Tests @@ -70,13 +76,17 @@ tests = [ # Tests models "joblib>=1.2.0,<1.3.0", - "pandas>=2.0.0,<3.0.0", - "scikit-learn>=1.2.0,<1.3.0", - "tensorflow>=2.0.0,<3.0.0", + "pandas>=2.0.0,<3.0.0;python_version>='3.8'", + "pandas;python_version<'3.8'", + "scikit-learn>=1.2.0,<1.3.0;python_version>='3.8'", + "scikit-learn;python_version<'3.8'", + "tensorflow>=2.0.0,<3.0.0;python_version>='3.8'", + "tensorflow;python_version<'3.8'", "lightgbm>=4.0.0,<5.0.0", - "torch>=2.0.0,<3.0.0", + "torch>=2.0.0,<3.0.0;python_version>='3.8'", + "torch;python_version<'3.8'", "onnx>=1.0.0,<2.0.0", - "onnxruntime>=1.0.0,<2.0.0", + "onnxruntime<1.16", "tf2onnx>=1.0.0,<2.0.0", "skl2onnx>=1.0.0,<2.0.0", diff --git a/tests/conftest.py b/tests/conftest.py index 06e9b5a..7e13595 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -361,7 +361,7 @@ def scikit_learn_binary_cls_model_onnx(models_datadir): try: import onnxruntime as onnxrt from mlup.ml.binarization.onnx import _InferenceSessionWithPredict - model = _InferenceSessionWithPredict(models_datadir / 'scikit-learn-binary_cls_model.onnx') + model = _InferenceSessionWithPredict(str(models_datadir / 'scikit-learn-binary_cls_model.onnx')) return ModelAndPath( models_datadir / 'scikit-learn-binary_cls_model.onnx', model, @@ -480,7 +480,7 @@ def pytorch_binary_cls_model_onnx(models_datadir): try: import onnxruntime as onnxrt from mlup.ml.binarization.onnx import _InferenceSessionWithPredict - model = _InferenceSessionWithPredict(models_datadir / 'pytorch-binary_cls_model.onnx') + model = _InferenceSessionWithPredict(str(models_datadir / 'pytorch-binary_cls_model.onnx')) return ModelAndPath( models_datadir / 'pytorch-binary_cls_model.onnx', model, diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 6b2de56..f0b31f8 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -52,7 +52,7 @@ def start_server(self): ip=self.ip, port=self.port, token=self.token, notebooks_folder=self.notebooks_folder, ).split() ) - logger.info(f'Waiting start jupyter notebooks server 5 seconds') + logger.info('Waiting start jupyter notebooks server 5 seconds') sleep(5) def copy_notebook_to_work_folder(self, notebook_name: str): diff --git a/tests/integration_tests/frameworks/test_lightgbm_model.py b/tests/integration_tests/frameworks/test_lightgbm_model.py index 65d1e35..991fab9 100644 --- a/tests/integration_tests/frameworks/test_lightgbm_model.py +++ b/tests/integration_tests/frameworks/test_lightgbm_model.py @@ -5,6 +5,7 @@ from mlup.constants import ModelDataTransformerType, BinarizationType, StorageType, WebAppArchitecture from mlup.up import UP, Config +from mlup.utils.loop import create_async_task try: import lightgbm as lgb @@ -146,7 +147,7 @@ async def test_web_app_with_batching_architecture(self, lightgbm_binary_cls_mode # Not found results. Long client wait response pred_id_1 = pred_resp_1.json()["predict_result"]["predict_id"] - task = asyncio.create_task(api_test_client.get("/get-predict/" + pred_id_1)) + task = create_async_task(api_test_client.get("/get-predict/" + pred_id_1)) await asyncio.sleep(0.2) if task.cancelled(): pytest.fail('Tasks could not have status cancelled.') diff --git a/tests/integration_tests/frameworks/test_pytorch_model.py b/tests/integration_tests/frameworks/test_pytorch_model.py index 33cf9f5..ebb8ae3 100644 --- a/tests/integration_tests/frameworks/test_pytorch_model.py +++ b/tests/integration_tests/frameworks/test_pytorch_model.py @@ -5,6 +5,7 @@ from mlup.constants import ModelDataTransformerType, BinarizationType, StorageType, WebAppArchitecture from mlup.up import UP, Config +from mlup.utils.loop import create_async_task try: import torch @@ -155,7 +156,7 @@ async def test_web_app_with_batching_architecture(self, pytorch_binary_cls_model # Not found results. Long client wait response pred_id_1 = pred_resp_1.json()["predict_result"]["predict_id"] - task = asyncio.create_task(api_test_client.get("/get-predict/" + pred_id_1)) + task = create_async_task(api_test_client.get("/get-predict/" + pred_id_1)) await asyncio.sleep(0.2) if task.cancelled(): pytest.fail('Tasks could not have status cancelled.') diff --git a/tests/integration_tests/frameworks/test_scikit_learn_model.py b/tests/integration_tests/frameworks/test_scikit_learn_model.py index e0544a9..1f4b434 100644 --- a/tests/integration_tests/frameworks/test_scikit_learn_model.py +++ b/tests/integration_tests/frameworks/test_scikit_learn_model.py @@ -5,6 +5,7 @@ from mlup.up import UP, Config from mlup.constants import WebAppArchitecture +from mlup.utils.loop import create_async_task try: import sklearn @@ -110,7 +111,7 @@ async def test_web_app_with_batching_architecture(self, scikit_learn_binary_cls_ # Not found results. Long client wait response pred_id_1 = pred_resp_1.json()["predict_result"]["predict_id"] - task = asyncio.create_task(api_test_client.get("/get-predict/" + pred_id_1)) + task = create_async_task(api_test_client.get("/get-predict/" + pred_id_1)) await asyncio.sleep(0.2) if task.cancelled(): pytest.fail('Tasks could not have status cancelled.') diff --git a/tests/integration_tests/frameworks/test_tensorflow_model.py b/tests/integration_tests/frameworks/test_tensorflow_model.py index 754db0a..c2b2a48 100644 --- a/tests/integration_tests/frameworks/test_tensorflow_model.py +++ b/tests/integration_tests/frameworks/test_tensorflow_model.py @@ -5,6 +5,7 @@ from mlup.constants import ModelDataTransformerType, StorageType, BinarizationType, WebAppArchitecture from mlup.up import UP, Config +from mlup.utils.loop import create_async_task try: import tensorflow @@ -160,7 +161,7 @@ async def test_web_app_with_batching_architecture(self, tensorflow_binary_cls_mo # Not found results. Long client wait response pred_id_1 = pred_resp_1.json()["predict_result"]["predict_id"] - task = asyncio.create_task(api_test_client.get("/get-predict/" + pred_id_1)) + task = create_async_task(api_test_client.get("/get-predict/" + pred_id_1)) await asyncio.sleep(0.2) if task.cancelled(): pytest.fail('Tasks could not have status cancelled.') diff --git a/tests/integration_tests/test_serilization.py b/tests/integration_tests/test_serilization.py index ec56ecf..6c74719 100644 --- a/tests/integration_tests/test_serilization.py +++ b/tests/integration_tests/test_serilization.py @@ -2,7 +2,11 @@ import os import pickle +import pytest + from mlup.up import UP, Config +from mlup.constants import ModelDataTransformerType, StorageType, BinarizationType, WebAppArchitecture +from mlup.ml.model import MLupModel, ModelConfig logger = logging.getLogger('mlup.test') try: @@ -11,11 +15,6 @@ logger.info(f'joblib library not installed. Skip test. {e}') joblib = None -import pytest - -from mlup.constants import ModelDataTransformerType, StorageType, BinarizationType, WebAppArchitecture -from mlup.ml.model import MLupModel, ModelConfig - @pytest.mark.asyncio async def test_mlup_model_pickle_serilization(pickle_print_model, models_datadir): diff --git a/tests/unit_tests/console_scripts/test_run.py b/tests/unit_tests/console_scripts/test_run.py index 47a54e5..75c5929 100644 --- a/tests/unit_tests/console_scripts/test_run.py +++ b/tests/unit_tests/console_scripts/test_run.py @@ -34,7 +34,7 @@ def test_run_from_config_not_exists_conf(config_path, config_type): def test_run_from_config_not_exists_conf_type(tmp_path_factory): file_path = tmp_path_factory.getbasetemp() / 'test_run_from_config_not_exists_conf_type' / \ - f'test_run_from_config_not_exists_conf_type.json' + 'test_run_from_config_not_exists_conf_type.json' os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w') as f: f.write('Not valid config\n\nBut is valid multirows string.') @@ -116,7 +116,7 @@ def test_run_from_up_bin_not_exists_bin(binary_path, binary_type): def test_run_from_up_bin_not_exists_binary_type(tmp_path_factory): file_path = tmp_path_factory.getbasetemp() / 'test_run_from_up_bin_not_exists_binary_type' / \ - f'test_run_from_up_bin_not_exists_binary_type.pckl' + 'test_run_from_up_bin_not_exists_binary_type.pckl' os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w') as f: f.write('Not valid binary\n\nBut is valid multirows string.') @@ -206,12 +206,12 @@ def test_run_from_model_not_exists_model(): run_from_model('not_exists_model.pckl') pytest.fail('Not raised error') except ModelLoadError as e: - assert str(e) == f"[Errno 2] No such file or directory: 'not_exists_model.pckl'" + assert str(e) == "[Errno 2] No such file or directory: 'not_exists_model.pckl'" def test_run_from_model_not_valid_model(tmp_path_factory): file_path = tmp_path_factory.getbasetemp() / 'test_run_from_model_not_valid_model' / \ - f'test_run_from_model_not_valid_model.pckl' + 'test_run_from_model_not_valid_model.pckl' os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w') as f: f.write('Not valid model\n\nBut is valid multirows string.') diff --git a/tests/unit_tests/console_scripts/test_validate_config.py b/tests/unit_tests/console_scripts/test_validate_config.py index 45110a2..b671c79 100644 --- a/tests/unit_tests/console_scripts/test_validate_config.py +++ b/tests/unit_tests/console_scripts/test_validate_config.py @@ -19,7 +19,7 @@ def test_validate_config_not_exists_conf(config_path, config_type): def test_validate_config_not_exists_conf_type(tmp_path_factory): - file_path = tmp_path_factory.getbasetemp() / f'test_validate_config_not_exists_conf_type.json' + file_path = tmp_path_factory.getbasetemp() / 'test_validate_config_not_exists_conf_type.json' with open(file_path, 'w') as f: f.write('Not valid config\n\nBut is valid multirows string.') diff --git a/tests/unit_tests/ml/test_binarization.py b/tests/unit_tests/ml/test_binarization.py index b185b8c..31a51fb 100644 --- a/tests/unit_tests/ml/test_binarization.py +++ b/tests/unit_tests/ml/test_binarization.py @@ -2,11 +2,13 @@ from io import BytesIO import pytest -logger = logging.getLogger('mlup.test') from mlup.constants import BinarizationType, LoadedFile from mlup.ml.binarization.memory import MemoryBinarizer from mlup.ml.binarization.pickle import PickleBinarizer +from mlup.utils.interspection import get_class_by_path + +logger = logging.getLogger('mlup.test') try: from mlup.ml.binarization.joblib import JoblibBinarizer except (ModuleNotFoundError, AttributeError) as e: @@ -33,8 +35,6 @@ logger.info(f'pytorch library not installed. Skip test. {e}') TorchBinarizer, TorchJITBinarizer = None, None -from mlup.utils.interspection import get_class_by_path - @pytest.fixture(scope="session") def model_not_readable_from_disk(tmp_path_factory): @@ -75,7 +75,8 @@ def test_single_file_pickle_binarization_deserialize(pickle_print_model): id='bytes' ), pytest.param( - BytesIO(b'\x80\x04\x95$\x00\x00\x00\x00\x00\x00\x00\x8c\x0etests.conftest\x94\x8c\nPrintModel\x94\x93\x94)\x81\x94.'), + BytesIO(b'\x80\x04\x95$\x00\x00\x00\x00\x00\x00\x00\x8c\x0etests.conftest\x94\x8c\nPrintModel\x94\x93\x94)' + b'\x81\x94.'), id='BufferedReader' ) ] diff --git a/tests/unit_tests/ml/test_data_transformers.py b/tests/unit_tests/ml/test_data_transformers.py index 0862d0f..d9004c5 100644 --- a/tests/unit_tests/ml/test_data_transformers.py +++ b/tests/unit_tests/ml/test_data_transformers.py @@ -7,6 +7,7 @@ from mlup.constants import ModelDataTransformerType from mlup.ml.data_transformers.src_data_transformer import SrcDataTransformer +from mlup.utils.interspection import get_class_by_path logger = logging.getLogger('mlup.test') @@ -32,6 +33,7 @@ except (ImportError, AttributeError) as e: logger.info(f'tensorflow not installed. Skip tests. {e}') tensorflow, TFTensorDataTransformer = None, None + def assert_tf_tensors(a, b, msg): assert False @@ -42,11 +44,10 @@ def assert_tf_tensors(a, b, msg): except (ImportError, AttributeError) as e: logger.info(f'PyTorch not installed. Skip tests. {e}') torch, TorchTensorDataTransformer = None, None + def is_equal_torch_tensors(a, b): return False -from mlup.utils.interspection import get_class_by_path - @pytest.mark.parametrize( 'data_type, expected_class', [ @@ -127,7 +128,7 @@ def test_transform_to_model_format_from_dict_without_columns(self): # Check order by first item reversed_data = data.copy() - reversed_data[0] = {k: v for k, v in reversed(reversed_data[0].items())} + reversed_data[0] = {k: v for k, v in reversed(list(reversed_data[0].items()))} pred_d = self.transformer_class().transform_to_model_format(reversed_data) assert pred_d == reversed_data @@ -276,7 +277,7 @@ def test_transform_to_model_format_from_dict_without_columns(self): # Check order by first item reversed_data = data.copy() - reversed_data[0] = {k: v for k, v in reversed(reversed_data[0].items())} + reversed_data[0] = {k: v for k, v in reversed(list(reversed_data[0].items()))} pred_d = self.transformer_class().transform_to_model_format(reversed_data) assert np.array_equal(pred_d, np.array([list(v.values())[::-1] for v in data])) @@ -369,7 +370,7 @@ def test_transform_to_model_format_from_dict_without_columns(self): # Check order by first item reversed_data = data.copy() - reversed_data[0] = {k: v for k, v in reversed(reversed_data[0].items())} + reversed_data[0] = {k: v for k, v in reversed(list(reversed_data[0].items()))} pred_d = self.transformer_class().transform_to_model_format(reversed_data) assert_tf_tensors(pred_d, tensorflow.convert_to_tensor([list(v.values())[::-1] for v in data])) @@ -464,7 +465,7 @@ def test_transform_to_model_format_from_dict_without_columns(self): # Check order by first item reversed_data = data.copy() - reversed_data[0] = {k: v for k, v in reversed(reversed_data[0].items())} + reversed_data[0] = {k: v for k, v in reversed(list(reversed_data[0].items()))} pred_d = self.transformer_class().transform_to_model_format(reversed_data) assert is_equal_torch_tensors(pred_d, torch.tensor([list(v.values())[::-1] for v in data])) diff --git a/tests/unit_tests/ml/test_model.py b/tests/unit_tests/ml/test_model.py index 0e031e5..e7a0985 100644 --- a/tests/unit_tests/ml/test_model.py +++ b/tests/unit_tests/ml/test_model.py @@ -11,7 +11,7 @@ from mlup.constants import ModelDataTransformerType, DEFAULT_X_ARG_NAME, StorageType, BinarizationType from mlup.errors import ModelLoadError, PredictTransformDataError from mlup.ml.model import MLupModel, ModelConfig - +from mlup.utils.loop import create_async_task logger = logging.getLogger('mlup.test') @@ -605,7 +605,7 @@ async def test_predict_with_not_correct_data_transformers( mlup_model.load() try: await mlup_model.predict(X=data) - pytest.fail(f'Not raised TransformDataError') + pytest.fail('Not raised TransformDataError') except PredictTransformDataError as e: assert str(e) == error_msg @@ -762,9 +762,9 @@ async def call_predict(mlup_model, data): start = time.monotonic() for i in range(3): tasks.append( - asyncio.create_task( + create_async_task( call_predict(mlup_model, {'X': [[1, 2, 3]]}), - name=f'test_predict_lock-not_sleep_{i}' + name=f'test_predict_lock-not_sleep_{i}', ) ) await asyncio.gather(*tasks) @@ -778,7 +778,7 @@ async def call_predict(mlup_model, data): start = time.monotonic() for i in range(3): tasks.append( - asyncio.create_task( + create_async_task( call_predict(mlup_model, {'X': [[1, 2, 3]]}), name=f'test_predict_lock-with_sleep_{i}' ) diff --git a/tests/unit_tests/ml/test_model_config.py b/tests/unit_tests/ml/test_model_config.py index 3f80826..188da9c 100644 --- a/tests/unit_tests/ml/test_model_config.py +++ b/tests/unit_tests/ml/test_model_config.py @@ -1,4 +1,4 @@ -from mlup.constants import ModelLibraryType, StorageType, BinarizationType, ModelDataTransformerType +from mlup.constants import ModelLibraryType, StorageType, ModelDataTransformerType from mlup.ml.model import ModelConfig diff --git a/tests/unit_tests/ml/test_storage.py b/tests/unit_tests/ml/test_storage.py index 2694067..0dddfbe 100644 --- a/tests/unit_tests/ml/test_storage.py +++ b/tests/unit_tests/ml/test_storage.py @@ -1,6 +1,7 @@ import logging import os import pickle +import shutil import pytest @@ -119,22 +120,30 @@ def test_load_single_file_by_not_default_mask(self, joblib_print_model): def test_load_many_file_by_not_default_mask( self, pickle_with_x_model, pickle_print_model, joblib_print_model, joblib_print_sleep_model ): + path_to_folder = os.path.join(os.path.dirname(joblib_print_model), 'test_load_many_file_by_not_default_mask') + os.makedirs(path_to_folder) + joblib_print_model_new_path = os.path.join(path_to_folder, os.path.basename(joblib_print_model)) + joblib_print_sleep_model_new_path = os.path.join(path_to_folder, os.path.basename(joblib_print_sleep_model)) + shutil.copyfile(joblib_print_model, joblib_print_model_new_path) + shutil.copyfile(joblib_print_sleep_model, joblib_print_sleep_model_new_path) + shutil.copyfile(pickle_with_x_model, os.path.join(path_to_folder, os.path.basename(pickle_with_x_model))) + shutil.copyfile(pickle_print_model, os.path.join(path_to_folder, os.path.basename(pickle_print_model))) + storage = DiskStorage( - path_to_files=os.path.dirname(joblib_print_model), + path_to_files=path_to_folder, files_mask=r'(\w.-_)*.joblib', need_load_file=True, ) models_bin = storage.load() - # assert len(models_bin) == 2 - with open(joblib_print_model, 'rb') as f: + assert len(models_bin) == 2 + with open(joblib_print_model_new_path, 'rb') as f: print_model_data = f.read() - with open(joblib_print_sleep_model, 'rb') as f: + with open(joblib_print_sleep_model_new_path, 'rb') as f: print_sleep_model_data = f.read() # Order in folder - assert str(models_bin[0].path) == str(joblib_print_model) - assert models_bin[0].raw_data == print_model_data - assert str(models_bin[1].path) == str(joblib_print_sleep_model) - assert models_bin[1].raw_data == print_sleep_model_data + for m in models_bin: + assert str(m.path) in (str(joblib_print_model_new_path), str(joblib_print_sleep_model_new_path)) + assert m.raw_data in (print_model_data, print_sleep_model_data) # Folder, file @pytest.mark.parametrize( diff --git a/tests/unit_tests/test_configs.py b/tests/unit_tests/test_configs.py index e53a89a..7769100 100644 --- a/tests/unit_tests/test_configs.py +++ b/tests/unit_tests/test_configs.py @@ -116,12 +116,13 @@ class ForExample: @dataclass class _ML: conf = _Conf() + @dataclass class _WEB: conf = _Conf() - ml: _ML = _ML() - web: _WEB = _WEB() + ml: _ML = field(default_factory=_ML) + web: _WEB = field(default_factory=_WEB) class TestConfigProvider: @@ -131,12 +132,13 @@ class NewForExample: @dataclass class _ML: conf = new_config + @dataclass class _WEB: conf = new_config - ml: _ML = _ML() - web: _WEB = _WEB() - return NewForExample + ml: _ML = field(default_factory=_ML) + web: _WEB = field(default_factory=_WEB) + return NewForExample() def test_get_config_dict(self): obj = ForExample() diff --git a/tests/unit_tests/utils/test_interspection.py b/tests/unit_tests/utils/test_interspection.py index cd082a6..cf459e2 100644 --- a/tests/unit_tests/utils/test_interspection.py +++ b/tests/unit_tests/utils/test_interspection.py @@ -1,4 +1,5 @@ from typing import List +import sys import pytest @@ -8,9 +9,14 @@ def pred_func_with_X_List(wt, x: List, b: bool = False): pass def pred_func_with_X_List_of_str(wt, x: List[str], b: bool = False): pass -def pred_func_with_list(wt, x: list, b: bool = False): pass -def pred_func_with_list_of_int(wt, x: list[int], b: bool = False): pass def pred_func_without_x(wt, y: List, b: bool = False): pass +def pred_func_with_list(wt, x: list, b: bool = False): pass + + +if sys.version_info.minor >= 9: + def pred_func_with_list_of_int(wt, x: list[int], b: bool = False): pass +else: + def pred_func_with_list_of_int(wt, x: List[int], b: bool = False): pass pred_func_args_without_auto_detect_predict_params = [ diff --git a/tests/unit_tests/web/test_api_docs.py b/tests/unit_tests/web/test_api_docs.py index dbbe649..43420bc 100644 --- a/tests/unit_tests/web/test_api_docs.py +++ b/tests/unit_tests/web/test_api_docs.py @@ -326,6 +326,7 @@ def test_generate_openapi_schema(print_model): up.ml.load() up.web.load() generated_scheme = generate_openapi_schema(up.web.app, up.ml) + openapi_full_scheme['openapi'] = generated_scheme['openapi'] assertDictEqual(generated_scheme, openapi_full_scheme) @@ -358,6 +359,7 @@ def test_generate_openapi_scheme_with_default_X_and_custom_columns(print_model): _scheme_columns_for_predict['required'] = openapi_required_cols _scheme_columns_for_predict['properties'] = openapi_cols + _openapi_full_scheme['openapi'] = generated_scheme['openapi'] assertDictEqual(generated_scheme, _openapi_full_scheme) @@ -386,7 +388,7 @@ def test_generate_openapi_scheme_with_default_X_without_columns(print_model): } } - + openapi_full_scheme['openapi'] = generated_scheme['openapi'] assertDictEqual(generated_scheme, _openapi_full_scheme) @@ -414,6 +416,7 @@ def test_generate_openapi_scheme_with_auto_analyze_X_with_custom_columns(print_m _scheme_columns_for_predict['required'] = openapi_required_cols _scheme_columns_for_predict['properties'] = openapi_cols + _openapi_full_scheme['openapi'] = generated_scheme['openapi'] assertDictEqual(generated_scheme, _openapi_full_scheme) @@ -427,6 +430,7 @@ def test_generate_openapi_scheme_with_auto_analyze_X_without_columns(print_model up.web.load() generated_scheme = generate_openapi_schema(up.web.app, up.ml) + openapi_full_scheme['openapi'] = generated_scheme['openapi'] assertDictEqual(generated_scheme, openapi_full_scheme) @@ -456,6 +460,7 @@ def test_generate_openapi_scheme_with_is_long_predict(print_model): } } + _openapi_full_scheme['openapi'] = generated_scheme['openapi'] assertDictEqual(generated_scheme, _openapi_full_scheme) @@ -537,4 +542,5 @@ async def test_api_handler(item_id: int, data: CustomRequestData): } } + _openapi_full_scheme['openapi'] = generated_scheme['openapi'] assertDictEqual(generated_scheme, _openapi_full_scheme) diff --git a/tests/unit_tests/web/test_app.py b/tests/unit_tests/web/test_app.py index f506d32..41a18d9 100644 --- a/tests/unit_tests/web/test_app.py +++ b/tests/unit_tests/web/test_app.py @@ -10,7 +10,7 @@ from mlup.constants import ModelDataTransformerType, ITEM_ID_COL_NAME, WebAppArchitecture from mlup.errors import WebAppLoadError from mlup.ml.model import MLupModel, ModelConfig -from mlup.utils.loop import run_async +from mlup.utils.loop import run_async, create_async_task from mlup.web.app import MLupWebApp, WebAppConfig @@ -114,11 +114,11 @@ def test_web_app_attribute(print_model): [ ( {'is_long_predict': False, 'mode': WebAppArchitecture.directly_to_predict}, - {'/predict': {'POST',}, '/info': {'GET',}} + {'/predict': {'POST', }, '/info': {'GET', }} ), ( {'is_long_predict': True, 'mode': WebAppArchitecture.worker_and_queue}, - {'/predict': {'POST',}, '/info': {'GET',}, '/get-predict/{predict_id}': {'GET',}} + {'/predict': {'POST', }, '/info': {'GET', }, '/get-predict/{predict_id}': {'GET', }} ), ], ids=['is_long_predict=False', 'is_long_predict=True'] @@ -441,7 +441,7 @@ async def test_predict_max_requests_throttling(web_app_test_client, print_sleep_ mlup_model.model_obj.sleep = 0.2 mlup_web_app.load() with web_app_test_client(mlup_web_app) as api_test_client: - first_request_task = asyncio.create_task( + first_request_task = create_async_task( api_test_client.post("/predict", json={'X': [[1, 2, 3]]}), name='test_predict_max_requests_throttling' ) diff --git a/tests/unit_tests/web/test_architecture.py b/tests/unit_tests/web/test_architecture.py index 34f8e95..1449479 100644 --- a/tests/unit_tests/web/test_architecture.py +++ b/tests/unit_tests/web/test_architecture.py @@ -10,6 +10,7 @@ from mlup.ml.model import MLupModel, ModelConfig from mlup.utils.collections import TTLOrderedDict from mlup.utils.interspection import get_class_by_path +from mlup.utils.loop import create_async_task from mlup.web.architecture.directly_to_predict import DirectlyToPredictArchitecture from mlup.web.architecture.worker_and_queue import WorkerAndQueueArchitecture from mlup.web.architecture.batching import BatchingSingleProcessArchitecture @@ -367,9 +368,9 @@ async def test_predict_with_little_ttl_predicted_data(self, print_model): assert pred_result_2 == [[1, 2, 3]] # Not found results - task = asyncio.create_task( + task = create_async_task( archi.get_predict_result(pred_id_1["predict_id"]), - name='test_predict_with_little_ttl_predicted_data' + name='test_predict_with_little_ttl_predicted_data', ) await asyncio.sleep(0.2) if task.cancelled(): @@ -843,7 +844,7 @@ async def test_predict_with_little_ttl_predicted_data(self, print_model): assert pred_result_2 == [[1, 2, 3]] # Not found results - task = asyncio.create_task( + task = create_async_task( archi.get_predict_result(pred_id_1["predict_id"]), name='test_predict_with_little_ttl_predicted_data' ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..5665936 --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[flake8] +max-line-length = 127 +per-file-ignores = + # Imported but unused + mlup/__init__.py: F401 + tests/conftest.py: F401