From 32111085d85de6cc17d0ea4dd08adba324f57590 Mon Sep 17 00:00:00 2001 From: Junghwan <70629228+shaun0927@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:05:50 +0900 Subject: [PATCH] Defer boto3 import and raise actionable ImportError for s3:// loads (#489) *Issue #, if available:* *Description of changes:* - `boto3` lives in `[project.optional-dependencies].extras`, but `src/chronos/boto_utils.py` imported it (and `botocore`) at module top. A user who installs the package with `pip install chronos-forecasting` (no extras) and then calls `from_pretrained("s3://...")` currently hits a bare `ModuleNotFoundError: No module named 'boto3'` with no hint about the `extras` group. - Defer `boto3` / `botocore` imports in `src/chronos/boto_utils.py` into the single function that actually uses them (`download_model_files_from_s3`) and move the `boto3.Session` annotation behind `from __future__ import annotations` + a `TYPE_CHECKING` block, so importing the package no longer fails when `boto3` is absent. - In `src/chronos/base.py` (inside `BaseChronosPipeline.from_pretrained`, around line 349) wrap the `cache_model_from_s3(...)` call in `try/except ImportError` and re-raise with a message that tells the user to run `pip install 'chronos-forecasting[extras]'`. HuggingFace Hub and local-path loads are unaffected. - Verified locally in a fresh venv (no `[extras]`): `from chronos import BaseChronosPipeline` succeeds, `BaseChronosPipeline.from_pretrained("s3://...")` now raises the actionable `ImportError` instead of the bare `ModuleNotFoundError`. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. Co-authored-by: Abdul Fatir --- src/chronos/base.py | 13 ++++++++++--- src/chronos/boto_utils.py | 15 +++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/chronos/base.py b/src/chronos/base.py index 2bba087..807f91d 100644 --- a/src/chronos/base.py +++ b/src/chronos/base.py @@ -348,9 +348,16 @@ class BaseChronosPipeline(metaclass=PipelineRegistry): if str(pretrained_model_name_or_path).startswith("s3://"): from .boto_utils import cache_model_from_s3 - local_model_path = cache_model_from_s3( - str(pretrained_model_name_or_path), force_download=force_s3_download - ) + try: + local_model_path = cache_model_from_s3( + str(pretrained_model_name_or_path), force_download=force_s3_download + ) + except ImportError as e: + raise ImportError( + "Loading models from s3:// URIs requires boto3. " + "Install the optional dependencies with: " + "pip install 'chronos-forecasting[extras]'" + ) from e return cls.from_pretrained(local_model_path, *model_args, **kwargs) from transformers import AutoConfig diff --git a/src/chronos/boto_utils.py b/src/chronos/boto_utils.py index d339333..48393d6 100644 --- a/src/chronos/boto_utils.py +++ b/src/chronos/boto_utils.py @@ -3,17 +3,19 @@ # Authors: Abdul Fatir Ansari +from __future__ import annotations + import logging import os import re import warnings from pathlib import Path +from typing import TYPE_CHECKING -import boto3 import requests # type: ignore -from botocore import UNSIGNED -from botocore.client import Config -from botocore.exceptions import ClientError, NoCredentialsError + +if TYPE_CHECKING: + import boto3 logger = logging.getLogger(__name__) @@ -57,6 +59,11 @@ def download_model_files_from_s3( force_download: bool = False, boto3_session: boto3.Session | None = None, ) -> None: + import boto3 + from botocore import UNSIGNED + from botocore.client import Config + from botocore.exceptions import ClientError, NoCredentialsError + boto3_session = boto3_session or boto3.Session() s3_client = boto3_session.client("s3")