From c062f601d6791f545db9f4df6e83032c3ee114ba Mon Sep 17 00:00:00 2001 From: AMARJEET J Date: Wed, 11 Nov 2026 19:56:43 +0000 Subject: [PATCH 1/3] fix(train): Stage tuner source in the trainer's bucket HyperparameterTuner uploaded the local sourcedir.tar.gz straight to session.default_bucket() under a key with no default_bucket_prefix. ModelTrainer.create_input_data_channel, which stages the sm_drivers channel for the same tuning job, resolves the bucket through _resolve_staging_bucket() (falling back to the output-path bucket when the training role cannot write the default bucket) and prefixes keys with default_bucket_prefix. The source archive now follows the same rules, so it lands next to sm_drivers and stays reachable by the role. PR #6559 fixed the KMS half of issue #6555; this is the bucket half. Closes #6555 X-AI-Prompt: Fix the open SDK issues from TT V2409806538 (#6555 bucket gap, #6554) X-AI-Tool: kiro-cli --- sagemaker-train/src/sagemaker/train/tuner.py | 12 ++++++-- .../unit/train/test_tuner_driver_channels.py | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/sagemaker-train/src/sagemaker/train/tuner.py b/sagemaker-train/src/sagemaker/train/tuner.py index f906c83317..652b71511e 100644 --- a/sagemaker-train/src/sagemaker/train/tuner.py +++ b/sagemaker-train/src/sagemaker/train/tuner.py @@ -577,8 +577,16 @@ def _build_driver_and_code_channels(cls, model_trainer): import tempfile session = model_trainer.sagemaker_session - bucket = session.default_bucket() - s3_key = f"{key_prefix}/sourcedir/sourcedir.tar.gz" + # Resolve bucket and prefix the same way as create_input_data_channel, so the + # source lands next to sm_drivers and honors default_bucket_prefix and the + # output-path fallback when the role can't write the default bucket (#6555). + source_key_prefix = f"{key_prefix}/sourcedir" + if session.default_bucket_prefix: + source_key_prefix = f"{session.default_bucket_prefix}/{source_key_prefix}" + bucket, staging_prefix = model_trainer._resolve_staging_bucket() + if staging_prefix: + source_key_prefix = f"{staging_prefix}/{source_key_prefix}" + s3_key = f"{source_key_prefix}/sourcedir.tar.gz" with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp: tar_path = tmp.name diff --git a/sagemaker-train/tests/unit/train/test_tuner_driver_channels.py b/sagemaker-train/tests/unit/train/test_tuner_driver_channels.py index 1b71aa8b06..60cdd7009f 100644 --- a/sagemaker-train/tests/unit/train/test_tuner_driver_channels.py +++ b/sagemaker-train/tests/unit/train/test_tuner_driver_channels.py @@ -89,6 +89,7 @@ def _mock_model_trainer(**overrides): trainer.tags = [] trainer.source_code = None trainer.configure_mock(**{"networking": None}) + trainer._resolve_staging_bucket.return_value = ("my-bucket", None) for k, v in overrides.items(): setattr(trainer, k, v) return trainer @@ -316,6 +317,33 @@ def test_uploads_sourcedir_without_kms_key(self, tmp_path): assert mock_s3_client.upload_file.call_args.kwargs["ExtraArgs"] is None + def test_uploads_sourcedir_to_resolved_staging_bucket(self, tmp_path): + """Source upload must follow the trainer's staging-bucket fallback (issue #6555).""" + trainer, mock_s3_client = self._upload_trainer(tmp_path, None) + trainer._resolve_staging_bucket.return_value = ("output-bucket", "jobs") + + HyperparameterTuner._build_driver_and_code_channels(trainer) + + trainer._resolve_staging_bucket.assert_called_once_with() + trainer.sagemaker_session.default_bucket.assert_not_called() + _, bucket, key = mock_s3_client.upload_file.call_args.args + assert bucket == "output-bucket" + assert key.startswith("jobs/test-tuning/") + assert key.endswith("/input/sourcedir/sourcedir.tar.gz") + assert trainer.hyperparameters["sagemaker_submit_directory"] == f"s3://output-bucket/{key}" + + def test_uploads_sourcedir_with_default_bucket_prefix(self, tmp_path): + """Source upload must honor the session's default_bucket_prefix (issue #6555).""" + trainer, mock_s3_client = self._upload_trainer(tmp_path, None) + trainer.sagemaker_session.default_bucket_prefix = "team-prefix" + + HyperparameterTuner._build_driver_and_code_channels(trainer) + + _, bucket, key = mock_s3_client.upload_file.call_args.args + assert bucket == "my-bucket" + assert key.startswith("team-prefix/test-tuning/") + assert key.endswith("/sourcedir/sourcedir.tar.gz") + def test_sets_submit_directory_for_s3_source_dir(self, tmp_path): """Should set sagemaker_submit_directory directly when source_dir is an S3 URI.""" source_code = _make_source_code(source_dir="s3://my-bucket/code/sourcedir.tar.gz") From 1a08ce6a9f332e9c4f339e0e8110d95c2d4a401f Mon Sep 17 00:00:00 2001 From: AMARJEET J Date: Wed, 11 Nov 2026 19:56:43 +0000 Subject: [PATCH 2/3] fix(core): Qualify shape Tag annotations in resources resources.py star-imports the shapes and later defines the resource class Tag (ListTags). Annotations written above that class, such as HyperParameterTuningJob.create(tags: Optional[List[Tag]]), meant the shape only because Python <= 3.13 evaluates annotations eagerly. Python 3.14 evaluates them lazily (PEP 649), so they resolve to resources.Tag and pydantic rejects the shapes.Tag objects HyperparameterTuner sends. Annotations written below the class already resolved to resources.Tag on every version (TrainingJob.create and eight others), which is why ModelTrainer had to pass dicts. Resource codegen now emits shapes listed in RESOURCE_SHADOWED_SHAPE_NAMES (only Tag) as shapes. and imports sagemaker.core.shapes; resources.py is regenerated, and the diff is only the 75 tags annotations plus that import. Every generated tags parameter now takes shapes.Tag or a dict on all Python versions. A guard test fails if a bare annotation names a shape that a later resource class shadows. Closes #6554 X-AI-Prompt: Fix the open SDK issues from TT V2409806538 (#6555 bucket gap, #6554) X-AI-Tool: kiro-cli --- .../src/sagemaker/core/resources.py | 151 +++++++++--------- .../src/sagemaker/core/tools/constants.py | 10 ++ .../sagemaker/core/tools/resources_codegen.py | 6 +- .../sagemaker/core/tools/shapes_extractor.py | 22 ++- .../test_resources_shadowed_shapes.py | 101 ++++++++++++ .../tests/unit/tools/test_shapes_extractor.py | 45 ++++++ .../tests/unit/train/test_model_trainer.py | 7 +- 7 files changed, 258 insertions(+), 84 deletions(-) create mode 100644 sagemaker-core/tests/unit/generated/test_resources_shadowed_shapes.py diff --git a/sagemaker-core/src/sagemaker/core/resources.py b/sagemaker-core/src/sagemaker/core/resources.py index dc6bca5f63..a12f14f8d9 100644 --- a/sagemaker-core/src/sagemaker/core/resources.py +++ b/sagemaker-core/src/sagemaker/core/resources.py @@ -24,6 +24,7 @@ from rich.status import Status from rich.style import Style from sagemaker.core.shapes import * +from sagemaker.core import shapes from sagemaker.core.helper.pipeline_variable import StrPipeVar from sagemaker.core.utils.code_injection.codec import transform from sagemaker.core.utils.code_injection.constants import Color @@ -199,7 +200,7 @@ class AIBenchmarkJob(Base): creation_time: Optional[datetime.datetime] = Unassigned() start_time: Optional[datetime.datetime] = Unassigned() end_time: Optional[datetime.datetime] = Unassigned() - tags: Optional[List[Tag]] = Unassigned() + tags: Optional[List[shapes.Tag]] = Unassigned() def get_name(self) -> str: attributes = vars(self) @@ -250,7 +251,7 @@ def create( ai_workload_config_identifier: StrPipeVar, role_arn: StrPipeVar, network_config: Optional[AIBenchmarkNetworkConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["AIBenchmarkJob"]: @@ -650,7 +651,7 @@ class AIRecommendationJob(Base): creation_time: Optional[datetime.datetime] = Unassigned() start_time: Optional[datetime.datetime] = Unassigned() end_time: Optional[datetime.datetime] = Unassigned() - tags: Optional[List[Tag]] = Unassigned() + tags: Optional[List[shapes.Tag]] = Unassigned() def get_name(self) -> str: attributes = vars(self) @@ -700,7 +701,7 @@ def create( optimize_model: Optional[bool] = Unassigned(), compute_spec: Optional[AIRecommendationComputeSpec] = Unassigned(), adapter_source: Optional[AIAdapterSource] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["AIRecommendationJob"]: @@ -1085,7 +1086,7 @@ class AIWorkloadConfig(Base): ai_workload_config_arn: Optional[StrPipeVar] = Unassigned() dataset_config: Optional[AIDatasetConfig] = Unassigned() ai_workload_configs: Optional[AIWorkloadConfigs] = Unassigned() - tags: Optional[List[Tag]] = Unassigned() + tags: Optional[List[shapes.Tag]] = Unassigned() creation_time: Optional[datetime.datetime] = Unassigned() def get_name(self) -> str: @@ -1111,7 +1112,7 @@ def create( ai_workload_config_name: StrPipeVar, dataset_config: Optional[AIDatasetConfig] = Unassigned(), ai_workload_configs: Optional[AIWorkloadConfigs] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["AIWorkloadConfig"]: @@ -1429,7 +1430,7 @@ def create( status: Optional[StrPipeVar] = Unassigned(), properties: Optional[Dict[StrPipeVar, StrPipeVar]] = Unassigned(), metadata_properties: Optional[MetadataProperties] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Action"]: @@ -1825,7 +1826,7 @@ def create( inference_specification: Optional[InferenceSpecification] = Unassigned(), validation_specification: Optional[AlgorithmValidationSpecification] = Unassigned(), certify_for_marketplace: Optional[bool] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Algorithm"]: @@ -2261,7 +2262,7 @@ def create( app_name: StrPipeVar, user_profile_name: Optional[Union[StrPipeVar, object]] = Unassigned(), space_name: Optional[Union[StrPipeVar, object]] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), resource_spec: Optional[ResourceSpec] = Unassigned(), recovery_mode: Optional[bool] = Unassigned(), session: Optional[Session] = None, @@ -2716,7 +2717,7 @@ def get_name(self) -> str: def create( cls, app_image_config_name: StrPipeVar, - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), kernel_gateway_image_config: Optional[KernelGatewayImageConfig] = Unassigned(), jupyter_lab_app_image_config: Optional[JupyterLabAppImageConfig] = Unassigned(), code_editor_app_image_config: Optional[CodeEditorAppImageConfig] = Unassigned(), @@ -3082,7 +3083,7 @@ def create( artifact_name: Optional[StrPipeVar] = Unassigned(), properties: Optional[Dict[StrPipeVar, StrPipeVar]] = Unassigned(), metadata_properties: Optional[MetadataProperties] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Artifact"]: @@ -3715,7 +3716,7 @@ def create( auto_ml_job_objective: Optional[AutoMLJobObjective] = Unassigned(), auto_ml_job_config: Optional[AutoMLJobConfig] = Unassigned(), generate_candidate_definitions_only: Optional[bool] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), model_deploy_config: Optional[ModelDeployConfig] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, @@ -4230,7 +4231,7 @@ def create( output_data_config: AutoMLOutputDataConfig, auto_ml_problem_type_config: AutoMLProblemTypeConfig, role_arn: StrPipeVar, - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), security_config: Optional[AutoMLSecurityConfig] = Unassigned(), auto_ml_job_objective: Optional[AutoMLJobObjective] = Unassigned(), model_deploy_config: Optional[ModelDeployConfig] = Unassigned(), @@ -4554,7 +4555,7 @@ def create( ClusterRestrictedInstanceGroupsConfig ] = Unassigned(), vpc_config: Optional[VpcConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), orchestrator: Optional[ClusterOrchestrator] = Unassigned(), node_recovery: Optional[StrPipeVar] = Unassigned(), tiered_storage_config: Optional[ClusterTieredStorageConfig] = Unassigned(), @@ -5315,7 +5316,7 @@ def create( cluster_arn: StrPipeVar, scheduler_config: SchedulerConfig, description: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["ClusterSchedulerConfig"]: @@ -5819,7 +5820,7 @@ def create( cls, code_repository_name: StrPipeVar, git_config: GitConfig, - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["CodeRepository"]: @@ -6211,7 +6212,7 @@ def create( model_package_version_arn: Optional[StrPipeVar] = Unassigned(), input_config: Optional[InputConfig] = Unassigned(), vpc_config: Optional[NeoVpcConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["CompilationJob"]: @@ -6640,7 +6641,7 @@ def create( compute_quota_target: ComputeQuotaTarget, description: Optional[StrPipeVar] = Unassigned(), activation_state: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["ComputeQuota"]: @@ -7159,7 +7160,7 @@ def create( context_type: StrPipeVar, description: Optional[StrPipeVar] = Unassigned(), properties: Optional[Dict[StrPipeVar, StrPipeVar]] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Context"]: @@ -7569,7 +7570,7 @@ def create( data_quality_baseline_config: Optional[DataQualityBaselineConfig] = Unassigned(), network_config: Optional[MonitoringNetworkConfig] = Unassigned(), stopping_condition: Optional[MonitoringStoppingCondition] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["DataQualityJobDefinition"]: @@ -8118,7 +8119,7 @@ def create( output_config: EdgeOutputConfig, role_arn: Optional[StrPipeVar] = Unassigned(), description: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), enable_iot_role_alias: Optional[bool] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, @@ -8523,7 +8524,7 @@ def get_report( def register_devices( self, devices: List[Device], - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[str] = None, ) -> None: @@ -8758,7 +8759,7 @@ def create( domain_settings: Optional[DomainSettings] = Unassigned(), subnet_ids: Optional[List[StrPipeVar]] = Unassigned(), vpc_id: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), app_network_access_type: Optional[StrPipeVar] = Unassigned(), home_efs_file_system_kms_key_id: Optional[StrPipeVar] = Unassigned(), kms_key_id: Optional[StrPipeVar] = Unassigned(), @@ -9258,7 +9259,7 @@ def create( model_configs: List[EdgeDeploymentModelConfig], device_fleet_name: Union[StrPipeVar, object], stages: Optional[List[DeploymentStage]] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["EdgeDeploymentPlan"]: @@ -9837,7 +9838,7 @@ def create( role_arn: StrPipeVar, output_config: EdgeOutputConfig, resource_key: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["EdgePackagingJob"]: @@ -10261,7 +10262,7 @@ def create( endpoint_name: StrPipeVar, endpoint_config_name: Union[StrPipeVar, object], deployment_config: Optional[DeploymentConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Endpoint"]: @@ -11096,7 +11097,7 @@ def create( endpoint_config_name: StrPipeVar, production_variants: List[ProductionVariant], data_capture_config: Optional[DataCaptureConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), kms_key_id: Optional[StrPipeVar] = Unassigned(), async_inference_config: Optional[AsyncInferenceConfig] = Unassigned(), explainer_config: Optional[ExplainerConfig] = Unassigned(), @@ -11419,7 +11420,7 @@ def create( experiment_name: StrPipeVar, display_name: Optional[StrPipeVar] = Unassigned(), description: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Experiment"]: @@ -11812,7 +11813,7 @@ def create( throughput_config: Optional[ThroughputConfig] = Unassigned(), role_arn: Optional[StrPipeVar] = Unassigned(), description: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["FeatureGroup"]: @@ -12920,7 +12921,7 @@ def create( human_loop_request_source: Optional[HumanLoopRequestSource] = Unassigned(), human_loop_activation_config: Optional[HumanLoopActivationConfig] = Unassigned(), human_loop_config: Optional[HumanLoopConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["FlowDefinition"]: @@ -13367,7 +13368,7 @@ def create( hub_display_name: Optional[StrPipeVar] = Unassigned(), hub_search_keywords: Optional[List[StrPipeVar]] = Unassigned(), s3_storage_config: Optional[HubS3StorageConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Hub"]: @@ -14131,7 +14132,7 @@ def load( hub_content_markdown: Optional[StrPipeVar] = Unassigned(), support_status: Optional[StrPipeVar] = Unassigned(), hub_content_search_keywords: Optional[List[StrPipeVar]] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["HubContent"]: @@ -14414,7 +14415,7 @@ class HubContentReference(Base): hub_content_arn: StrPipeVar hub_content_name: Optional[Union[StrPipeVar, object]] = Unassigned() min_version: Optional[StrPipeVar] = Unassigned() - tags: Optional[List[Tag]] = Unassigned() + tags: Optional[List[shapes.Tag]] = Unassigned() def get_name(self) -> str: attributes = vars(self) @@ -14440,7 +14441,7 @@ def create( sage_maker_public_hub_content_arn: StrPipeVar, hub_content_name: Optional[Union[StrPipeVar, object]] = Unassigned(), min_version: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[str] = None, ) -> Optional["HubContentReference"]: @@ -14627,7 +14628,7 @@ def create( cls, human_task_ui_name: StrPipeVar, ui_template: UiTemplate, - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["HumanTaskUi"]: @@ -15089,7 +15090,7 @@ def create( List[HyperParameterTrainingJobDefinition] ] = Unassigned(), warm_start_config: Optional[HyperParameterTuningJobWarmStartConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), autotune: Optional[Autotune] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, @@ -15643,7 +15644,7 @@ def create( role_arn: StrPipeVar, description: Optional[StrPipeVar] = Unassigned(), display_name: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Image"]: @@ -16693,7 +16694,7 @@ def create( specification: Optional[InferenceComponentSpecification] = Unassigned(), specifications: Optional[List[InferenceComponentSpecification]] = Unassigned(), runtime_config: Optional[InferenceComponentRuntimeConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["InferenceComponent"]: @@ -17273,7 +17274,7 @@ def create( description: Optional[StrPipeVar] = Unassigned(), data_storage_config: Optional[InferenceExperimentDataStorageConfig] = Unassigned(), kms_key: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["InferenceExperiment"]: @@ -17833,7 +17834,7 @@ def create( job_description: Optional[StrPipeVar] = Unassigned(), stopping_conditions: Optional[RecommendationJobStoppingConditions] = Unassigned(), output_config: Optional[RecommendationJobOutputConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["InferenceRecommendationsJob"]: @@ -18327,7 +18328,7 @@ class Job(Base): secondary_status: Optional[StrPipeVar] = Unassigned() secondary_status_transitions: Optional[List[JobSecondaryStatusTransition]] = Unassigned() failure_reason: Optional[StrPipeVar] = Unassigned() - tags: Optional[List[Tag]] = Unassigned() + tags: Optional[List[shapes.Tag]] = Unassigned() def get_name(self) -> str: attributes = vars(self) @@ -18368,7 +18369,7 @@ def create( job_category: StrPipeVar, job_config_schema_version: StrPipeVar, job_config_document: StrPipeVar, - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Job"]: @@ -18840,7 +18841,7 @@ class LabelingJob(Base): stopping_conditions: Optional[LabelingJobStoppingConditions] = Unassigned() labeling_job_algorithms_config: Optional[LabelingJobAlgorithmsConfig] = Unassigned() human_task_config: Optional[HumanTaskConfig] = Unassigned() - tags: Optional[List[Tag]] = Unassigned() + tags: Optional[List[shapes.Tag]] = Unassigned() labeling_job_output: Optional[LabelingJobOutput] = Unassigned() def get_name(self) -> str: @@ -18907,7 +18908,7 @@ def create( label_category_config_s3_uri: Optional[StrPipeVar] = Unassigned(), stopping_conditions: Optional[LabelingJobStoppingConditions] = Unassigned(), labeling_job_algorithms_config: Optional[LabelingJobAlgorithmsConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["LabelingJob"]: @@ -19569,7 +19570,7 @@ def create( weekly_maintenance_window_start: Optional[StrPipeVar] = Unassigned(), account_default_status: Optional[StrPipeVar] = Unassigned(), default_domain_id_list: Optional[List[StrPipeVar]] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["MlflowApp"]: @@ -20116,7 +20117,7 @@ def create( mlflow_version: Optional[StrPipeVar] = Unassigned(), automatic_model_registration: Optional[bool] = Unassigned(), weekly_maintenance_window_start: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), s3_bucket_owner_account_id: Optional[StrPipeVar] = Unassigned(), s3_bucket_owner_verification: Optional[bool] = Unassigned(), session: Optional[Session] = None, @@ -20739,7 +20740,7 @@ def create( containers: Optional[List[ContainerDefinition]] = Unassigned(), inference_execution_config: Optional[InferenceExecutionConfig] = Unassigned(), execution_role_arn: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), vpc_config: Optional[VpcConfig] = Unassigned(), enable_network_isolation: Optional[bool] = Unassigned(), session: Optional[Session] = None, @@ -21149,7 +21150,7 @@ def create( model_bias_baseline_config: Optional[ModelBiasBaselineConfig] = Unassigned(), network_config: Optional[MonitoringNetworkConfig] = Unassigned(), stopping_condition: Optional[MonitoringStoppingCondition] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["ModelBiasJobDefinition"]: @@ -21490,7 +21491,7 @@ def create( content: StrPipeVar, model_card_status: StrPipeVar, security_config: Optional[ModelCardSecurityConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["ModelCard"]: @@ -22389,7 +22390,7 @@ def create( ] = Unassigned(), network_config: Optional[MonitoringNetworkConfig] = Unassigned(), stopping_condition: Optional[MonitoringStoppingCondition] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["ModelExplainabilityJobDefinition"]: @@ -22819,7 +22820,7 @@ def create( validation_specification: Optional[ModelPackageValidationSpecification] = Unassigned(), source_algorithm_specification: Optional[SourceAlgorithmSpecification] = Unassigned(), certify_for_marketplace: Optional[bool] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), model_approval_status: Optional[StrPipeVar] = Unassigned(), metadata_properties: Optional[MetadataProperties] = Unassigned(), model_metrics: Optional[ModelMetrics] = Unassigned(), @@ -23431,7 +23432,7 @@ def create( cls, model_package_group_name: StrPipeVar, model_package_group_description: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), managed_configuration: Optional[ManagedConfiguration] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, @@ -24032,7 +24033,7 @@ def create( model_quality_baseline_config: Optional[ModelQualityBaselineConfig] = Unassigned(), network_config: Optional[MonitoringNetworkConfig] = Unassigned(), stopping_condition: Optional[MonitoringStoppingCondition] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["ModelQualityJobDefinition"]: @@ -24747,7 +24748,7 @@ def create( cls, monitoring_schedule_name: StrPipeVar, monitoring_schedule_config: MonitoringScheduleConfig, - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["MonitoringSchedule"]: @@ -25298,7 +25299,7 @@ def create( security_group_ids: Optional[List[StrPipeVar]] = Unassigned(), ip_address_type: Optional[StrPipeVar] = Unassigned(), kms_key_id: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), lifecycle_config_name: Optional[StrPipeVar] = Unassigned(), direct_internet_access: Optional[StrPipeVar] = Unassigned(), volume_size_in_gb: Optional[int] = Unassigned(), @@ -25931,7 +25932,7 @@ def create( notebook_instance_lifecycle_config_name: StrPipeVar, on_create: Optional[List[NotebookInstanceLifecycleHook]] = Unassigned(), on_start: Optional[List[NotebookInstanceLifecycleHook]] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["NotebookInstanceLifecycleConfig"]: @@ -26337,7 +26338,7 @@ def create( stopping_condition: StoppingCondition, max_instance_count: Optional[int] = Unassigned(), optimization_environment: Optional[Dict[StrPipeVar, StrPipeVar]] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), vpc_config: Optional[OptimizationVpcConfig] = Unassigned(), training_plan_arns: Optional[List[StrPipeVar]] = Unassigned(), session: Optional[Session] = None, @@ -26812,7 +26813,7 @@ def create( enable_iam_session_based_identity: Optional[bool] = Unassigned(), enable_auto_minor_version_upgrade: Optional[bool] = Unassigned(), client_token: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["PartnerApp"]: @@ -26999,7 +27000,7 @@ def update( enable_auto_minor_version_upgrade: Optional[bool] = Unassigned(), app_version: Optional[StrPipeVar] = Unassigned(), client_token: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), ) -> Optional["PartnerApp"]: """ Update a PartnerApp resource @@ -27421,7 +27422,7 @@ def create( pipeline_definition: Optional[StrPipeVar] = Unassigned(), pipeline_definition_s3_location: Optional[PipelineDefinitionS3Location] = Unassigned(), pipeline_description: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), parallelism_configuration: Optional[ParallelismConfiguration] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, @@ -29052,7 +29053,7 @@ def create( stopping_condition: Optional[ProcessingStoppingCondition] = Unassigned(), environment: Optional[Dict[StrPipeVar, StrPipeVar]] = Unassigned(), network_config: Optional[NetworkConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), experiment_config: Optional[ExperimentConfig] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, @@ -29507,7 +29508,7 @@ def create( service_catalog_provisioning_details: Optional[ ServiceCatalogProvisioningDetails ] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), template_providers: Optional[List[CreateTemplateProvider]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, @@ -29665,7 +29666,7 @@ def update( service_catalog_provisioning_update_details: Optional[ ServiceCatalogProvisioningUpdateDetails ] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), template_providers_to_update: Optional[List[UpdateTemplateProvider]] = Unassigned(), ) -> Optional["Project"]: """ @@ -30153,7 +30154,7 @@ def create( cls, domain_id: StrPipeVar, space_name: StrPipeVar, - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), space_settings: Optional[SpaceSettings] = Unassigned(), ownership_settings: Optional[OwnershipSettings] = Unassigned(), space_sharing_settings: Optional[SpaceSharingSettings] = Unassigned(), @@ -30644,7 +30645,7 @@ def create( studio_lifecycle_config_name: StrPipeVar, studio_lifecycle_config_content: StrPipeVar, studio_lifecycle_config_app_type: StrPipeVar, - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["StudioLifecycleConfig"]: @@ -31173,7 +31174,7 @@ def get_all( def add_tags( cls, resource_arn: StrPipeVar, - tags: List[Tag], + tags: List[shapes.Tag], session: Optional[Session] = None, region: Optional[str] = None, ) -> None: @@ -31429,7 +31430,7 @@ def create( resource_config: Optional[ResourceConfig] = Unassigned(), vpc_config: Optional[VpcConfig] = Unassigned(), stopping_condition: Optional[StoppingCondition] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), enable_network_isolation: Optional[bool] = Unassigned(), enable_inter_container_traffic_encryption: Optional[bool] = Unassigned(), enable_managed_spot_training: Optional[bool] = Unassigned(), @@ -32094,7 +32095,7 @@ def create( training_plan_name: StrPipeVar, training_plan_offering_id: StrPipeVar, spare_instance_count_per_ultra_server: Optional[int] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["TrainingPlan"]: @@ -32435,7 +32436,7 @@ def create( output_config: TrainingSessionOutputConfig, model_package_group_arn: Optional[StrPipeVar] = Unassigned(), vpc_config: Optional[VpcConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["TrainingSession"]: @@ -32907,7 +32908,7 @@ def create( environment: Optional[Dict[StrPipeVar, StrPipeVar]] = Unassigned(), data_capture_config: Optional[BatchDataCaptureConfig] = Unassigned(), data_processing: Optional[DataProcessing] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), experiment_config: Optional[ExperimentConfig] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, @@ -33327,7 +33328,7 @@ def create( experiment_name: Union[StrPipeVar, object], display_name: Optional[StrPipeVar] = Unassigned(), metadata_properties: Optional[MetadataProperties] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Trial"]: @@ -33704,7 +33705,7 @@ def create( input_artifacts: Optional[Dict[StrPipeVar, TrialComponentArtifact]] = Unassigned(), output_artifacts: Optional[Dict[StrPipeVar, TrialComponentArtifact]] = Unassigned(), metadata_properties: Optional[MetadataProperties] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["TrialComponent"]: @@ -34376,7 +34377,7 @@ def create( user_profile_name: StrPipeVar, single_sign_on_user_identifier: Optional[StrPipeVar] = Unassigned(), single_sign_on_user_value: Optional[StrPipeVar] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), user_settings: Optional[UserSettings] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, @@ -34882,7 +34883,7 @@ def create( cognito_config: Optional[CognitoConfig] = Unassigned(), oidc_config: Optional[OidcConfig] = Unassigned(), source_ip_config: Optional[SourceIpConfig] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), workforce_vpc_config: Optional[WorkforceVpcConfigRequest] = Unassigned(), ip_address_type: Optional[StrPipeVar] = Unassigned(), session: Optional[Session] = None, @@ -35343,7 +35344,7 @@ def create( workforce_name: Optional[Union[StrPipeVar, object]] = Unassigned(), notification_configuration: Optional[NotificationConfiguration] = Unassigned(), worker_access_configuration: Optional[WorkerAccessConfiguration] = Unassigned(), - tags: Optional[List[Tag]] = Unassigned(), + tags: Optional[List[shapes.Tag]] = Unassigned(), session: Optional[Session] = None, region: Optional[StrPipeVar] = None, ) -> Optional["Workteam"]: diff --git a/sagemaker-core/src/sagemaker/core/tools/constants.py b/sagemaker-core/src/sagemaker/core/tools/constants.py index fe07f0f0a4..16f7a8e453 100644 --- a/sagemaker-core/src/sagemaker/core/tools/constants.py +++ b/sagemaker-core/src/sagemaker/core/tools/constants.py @@ -53,6 +53,16 @@ BASIC_RETURN_TYPES = {"str", "int", "bool", "float", "datetime.datetime"} SHAPE_DAG_FILE_PATH = os.getcwd() + "/src/sagemaker/core/utils/code_injection/shape_dag.py" + +# Shape names that a resource class of the same name shadows inside resources.py. +# resources.py star-imports the shapes, then later defines ``class Tag(Base)`` (the +# ListTags resource). An unqualified ``Tag`` annotation written before that class means +# the shape only while annotations are evaluated eagerly; Python 3.14 evaluates them +# lazily (PEP 649), so it resolves to the resource class and rejects shape instances +# (issue #6554). Resource codegen emits these names as ``shapes.``. Other shared +# names (Device, Workteam, ...) are only referenced after their resource class, so they +# already resolve to the resource class on every Python version and are left alone. +RESOURCE_SHADOWED_SHAPE_NAMES = frozenset({"Tag"}) PYTHON_TYPES_TO_BASIC_JSON_TYPES = { "str": "string", "StrPipeVar": "string", diff --git a/sagemaker-core/src/sagemaker/core/tools/resources_codegen.py b/sagemaker-core/src/sagemaker/core/tools/resources_codegen.py index f981001332..d5bcb2f586 100644 --- a/sagemaker-core/src/sagemaker/core/tools/resources_codegen.py +++ b/sagemaker-core/src/sagemaker/core/tools/resources_codegen.py @@ -29,6 +29,7 @@ LOGGER_STRING, CONFIG_SCHEMA_FILE_NAME, PYTHON_TYPES_TO_BASIC_JSON_TYPES, + RESOURCE_SHADOWED_SHAPE_NAMES, CONFIGURABLE_ATTRIBUTE_SUBSTRINGS, RESOURCE_WITH_LOGS, DEFAULT_TIMEOUT_MESSAGE, @@ -154,7 +155,7 @@ def __init__(self, service_json: dict): # Initialize the resources and shapes extractors self.resources_extractor = ResourcesExtractor() - self.shapes_extractor = ShapesExtractor() + self.shapes_extractor = ShapesExtractor(qualified_shape_names=RESOURCE_SHADOWED_SHAPE_NAMES) # Extract the resources plan and shapes DAG self.resources_plan = self.resources_extractor.get_resource_plan() @@ -196,6 +197,9 @@ def generate_imports(self) -> str: "from rich.status import Status", "from rich.style import Style", "from sagemaker.core.shapes import *", + # Qualified access for shape names a resource class shadows (see + # RESOURCE_SHADOWED_SHAPE_NAMES), e.g. ``shapes.Tag`` vs resource ``Tag``. + "from sagemaker.core import shapes", "from sagemaker.core.helper.pipeline_variable import StrPipeVar", "from sagemaker.core.utils.code_injection.codec import transform", "from sagemaker.core.utils.code_injection.constants import Color", diff --git a/sagemaker-core/src/sagemaker/core/tools/shapes_extractor.py b/sagemaker-core/src/sagemaker/core/tools/shapes_extractor.py index fb35676810..86392fedc9 100644 --- a/sagemaker-core/src/sagemaker/core/tools/shapes_extractor.py +++ b/sagemaker-core/src/sagemaker/core/tools/shapes_extractor.py @@ -34,12 +34,20 @@ class ShapesExtractor: """Extracts the shapes to DAG structure.""" - def __init__(self, combined_shapes: Optional[dict] = None): + def __init__( + self, + combined_shapes: Optional[dict] = None, + qualified_shape_names: frozenset = frozenset(), + ): """Initializes a new instance of the ShapesExtractor class. :param combined_shapes: All the shapes put together from all Sagemaker Service JSONs + :param qualified_shape_names: Structure shape names to emit as ``shapes.`` + in type annotations. Used by resource codegen for shapes that a resource + class of the same name shadows in resources.py. Empty for shape codegen. """ self.combined_shapes = combined_shapes or load_combined_shapes_data() + self.qualified_shape_names = qualified_shape_names self.shape_dag = self.get_shapes_dag() with open(SHAPE_DAG_FILE_PATH, "w") as f: @@ -121,6 +129,12 @@ def get_shapes_dag(self): _dag[shape]["value_type"] = _all_shapes[_map_value_shape]["type"] return _dag + def _structure_type(self, shape_name): + """Return the annotation text for a structure shape, qualified when shadowed.""" + if shape_name in self.qualified_shape_names: + return f"shapes.{shape_name}" + return shape_name + def _evaluate_list_type(self, member_shape): list_shape_name = member_shape["member"]["shape"] list_shape_member = self.combined_shapes[list_shape_name] @@ -134,7 +148,7 @@ def _evaluate_list_type(self, member_shape): if list_shape_name == "SearchExpression": member_type = f"List['{list_shape_name}']" else: - member_type = f"List[{list_shape_name}]" + member_type = f"List[{self._structure_type(list_shape_name)}]" elif list_shape_type in BASIC_JSON_TYPES_TO_PYTHON_TYPES.keys(): member_type = f"List[{BASIC_JSON_TYPES_TO_PYTHON_TYPES[list_shape_type]}]" else: @@ -158,7 +172,7 @@ def _evaluate_map_type(self, member_shape): if map_value_shape_type == "structure": member_type = ( f"Dict[{BASIC_JSON_TYPES_TO_PYTHON_TYPES[map_key_shape_type]}, " - f"{map_value_shape_name}]" + f"{self._structure_type(map_value_shape_name)}]" ) elif map_value_shape_type == "list": member_type = ( @@ -238,7 +252,7 @@ def generate_shape_members(self, shape, required_override=()): member_shape = self.combined_shapes[member_shape_name] member_shape_type = member_shape["type"] if member_shape_type == "structure": - member_type = member_shape_name + member_type = self._structure_type(member_shape_name) elif member_shape_type == "list": member_type = self._evaluate_list_type(member_shape) elif member_shape_type == "map": diff --git a/sagemaker-core/tests/unit/generated/test_resources_shadowed_shapes.py b/sagemaker-core/tests/unit/generated/test_resources_shadowed_shapes.py new file mode 100644 index 0000000000..7285943638 --- /dev/null +++ b/sagemaker-core/tests/unit/generated/test_resources_shadowed_shapes.py @@ -0,0 +1,101 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Guard resource annotations against shape names shadowed by resource classes. + +resources.py star-imports every shape and then defines resource classes, some of which +share a shape's name (``Tag``). Before Python 3.14 an annotation is evaluated when its +function or class is defined, so a bare ``Tag`` written above ``class Tag`` means the +shape. Python 3.14 evaluates annotations lazily (PEP 649), so the same text resolves to +the resource class and rejects shape instances (issue #6554). +""" +import ast +import inspect +import typing + +import pytest +from pydantic import TypeAdapter + +from sagemaker.core import resources, shapes + + +def _resource_methods_with_tags(): + for _, cls in inspect.getmembers(resources, inspect.isclass): + if cls.__module__ != resources.__name__: + continue + for method_name in ("create", "add_tags"): + method = cls.__dict__.get(method_name) + if method is None: + continue + func = method.__func__ if isinstance(method, classmethod) else method + func = inspect.unwrap(func) + if "tags" in inspect.signature(func).parameters: + yield f"{cls.__name__}.{method_name}", func + + +TAGGED_METHODS = sorted(_resource_methods_with_tags()) + + +def test_no_shape_name_used_before_its_shadowing_resource_class(): + """A bare annotation name must not change meaning between eager and lazy evaluation.""" + tree = ast.parse(inspect.getsource(resources)) + shape_names = {name for name, obj in vars(shapes).items() if inspect.isclass(obj)} + shadowing_class_line = { + node.name: node.lineno + for node in tree.body + if isinstance(node, ast.ClassDef) and node.name in shape_names and node.name != "Base" + } + + def annotations(node): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + args = node.args + for arg in args.posonlyargs + args.args + args.kwonlyargs: + yield arg.annotation + yield node.returns + elif isinstance(node, ast.AnnAssign): + yield node.annotation + + offenders = set() + for node in ast.walk(tree): + for annotation in annotations(node): + if annotation is None: + continue + for name in ast.walk(annotation): + if ( + isinstance(name, ast.Name) + and name.id in shadowing_class_line + and name.lineno < shadowing_class_line[name.id] + ): + offenders.add((name.id, name.lineno)) + + assert not offenders, ( + "Annotations reference a shape name that a later resource class shadows; add the " + f"name to RESOURCE_SHADOWED_SHAPE_NAMES and regenerate: {sorted(offenders)[:10]}" + ) + + +def test_tagged_methods_found(): + assert len(TAGGED_METHODS) > 50 + assert "HyperParameterTuningJob.create" in dict(TAGGED_METHODS) + + +@pytest.mark.parametrize("name,func", TAGGED_METHODS, ids=[name for name, _ in TAGGED_METHODS]) +def test_tags_annotation_resolves_to_shape_tag(name, func): + """Every generated ``tags`` parameter takes the shape Tag on every Python version.""" + tags_type = typing.get_type_hints(func)["tags"] + adapter = TypeAdapter(tags_type) + + validated = adapter.validate_python([shapes.Tag(key="project", value="p1")]) + assert type(validated[0]) is shapes.Tag + + validated = adapter.validate_python([{"key": "project", "value": "p1"}]) + assert type(validated[0]) is shapes.Tag diff --git a/sagemaker-core/tests/unit/tools/test_shapes_extractor.py b/sagemaker-core/tests/unit/tools/test_shapes_extractor.py index 84cebacee7..35b1b28b97 100644 --- a/sagemaker-core/tests/unit/tools/test_shapes_extractor.py +++ b/sagemaker-core/tests/unit/tools/test_shapes_extractor.py @@ -492,3 +492,48 @@ def test_other_members_stay_required(self, extractor): assert members["capture_status"] == "StrPipeVar" assert members["current_sampling_percentage"] == "int" assert members["destination_s3_uri"] == "StrPipeVar" + + +class TestQualifiedShapeNames: + """Shapes that a resource class shadows are emitted as ``shapes.`` (issue #6554).""" + + _SHAPES = { + "Holder": { + "type": "structure", + "members": { + "Tag": {"shape": "Tag"}, + "Tags": {"shape": "TagList"}, + "TagMap": {"shape": "TagMap"}, + "Other": {"shape": "Other"}, + }, + }, + "Tag": {"type": "structure", "members": {}}, + "Other": {"type": "structure", "members": {}}, + "TagList": {"type": "list", "member": {"shape": "Tag"}}, + "TagMap": {"type": "map", "key": {"shape": "String"}, "value": {"shape": "Tag"}}, + "String": {"type": "string"}, + } + + @patch("sagemaker.core.tools.shapes_extractor.reformat_file_with_black") + def test_qualifies_only_listed_shapes(self, mock_reformat): + with patch("builtins.open", create=True): + extractor = ShapesExtractor( + combined_shapes=self._SHAPES, qualified_shape_names=frozenset({"Tag"}) + ) + + members = extractor.generate_shape_members("Holder") + + assert members["tag"] == "Optional[shapes.Tag] = Unassigned()" + assert members["tags"] == "Optional[List[shapes.Tag]] = Unassigned()" + assert members["tag_map"] == "Optional[Dict[StrPipeVar, shapes.Tag]] = Unassigned()" + assert members["other"] == "Optional[Other] = Unassigned()" + + @patch("sagemaker.core.tools.shapes_extractor.reformat_file_with_black") + def test_default_leaves_names_unqualified(self, mock_reformat): + """Shape codegen (no qualified names) must keep emitting bare names.""" + with patch("builtins.open", create=True): + extractor = ShapesExtractor(combined_shapes=self._SHAPES) + + members = extractor.generate_shape_members("Holder") + + assert members["tags"] == "Optional[List[Tag]] = Unassigned()" diff --git a/sagemaker-train/tests/unit/train/test_model_trainer.py b/sagemaker-train/tests/unit/train/test_model_trainer.py index 2bb0e6246f..404a4a7406 100644 --- a/sagemaker-train/tests/unit/train/test_model_trainer.py +++ b/sagemaker-train/tests/unit/train/test_model_trainer.py @@ -2795,10 +2795,9 @@ def test_boto3_request_uses_boto_style_tag_keys(model_trainer): def test_interactive_request_keeps_tags_training_job_create_accepts(model_trainer): """The interactive request carries dicts, not Tag models. - ``TrainingJob.create`` annotates ``tags`` with ``sagemaker.core.resources.Tag``, - which shadows the shape of the same name, so a ``sagemaker.core.shapes.Tag`` - instance fails its validation. The dicts are revalidated into that model instead, - and come out boto-style on the wire. + ``TrainingJob.create`` validates ``tags`` as ``sagemaker.core.shapes.Tag`` (it used + to resolve to the shadowing ``sagemaker.core.resources.Tag``, see issue #6554). The + dicts are revalidated into that model and come out boto-style on the wire. """ model_trainer.tags = [TAG] From 23144e70e7278893cd820744535b1930a1526253 Mon Sep 17 00:00:00 2001 From: AMARJEET J Date: Thu, 12 Nov 2026 17:57:30 +0000 Subject: [PATCH 3/3] style(core): Add blank line after test module docstring black 26.3.1 (pinned in requirements/tox/black_requirements.txt) requires an empty line after a module docstring; the codestyle-doc-tests (sagemaker-core) check failed on it. X-AI-Prompt: Fix the failing sagemaker-core doc test on PR 6578 X-AI-Tool: kiro-cli --- .../tests/unit/generated/test_resources_shadowed_shapes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sagemaker-core/tests/unit/generated/test_resources_shadowed_shapes.py b/sagemaker-core/tests/unit/generated/test_resources_shadowed_shapes.py index 7285943638..7ade77d512 100644 --- a/sagemaker-core/tests/unit/generated/test_resources_shadowed_shapes.py +++ b/sagemaker-core/tests/unit/generated/test_resources_shadowed_shapes.py @@ -18,6 +18,7 @@ shape. Python 3.14 evaluates annotations lazily (PEP 649), so the same text resolves to the resource class and rejects shape instances (issue #6554). """ + import ast import inspect import typing