Skip to content

Conversation

@evelyn-ys
Copy link
Member

@evelyn-ys evelyn-ys commented Feb 19, 2021

Description

Fix #14054
In previous design, if get_sdk can't find the dependency, the default behavior will return None. This will cause follow-up questions if None is not handled.
This PR exposes the real exception while getting sdk, which is ModuleNotFoundError

Testing Guide

> az storage container create -n ystesting
The command failed with an unexpected error. Here is the traceback:
'NoneType' object has no attribute '__name__'
Traceback (most recent call last):
  File "D:\workspace\ysenv\lib\site-packages\knack\cli.py", line 233, in invoke
    cmd_result = self.invocation.execute(args)
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 664, in execute
    raise ex
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 727, in _run_jobs_serially
    results.append(self._run_job(expanded_arg, cmd_copy))
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 719, in _run_job
    return cmd_copy.exception_handler(ex)
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\__init__.py", line 338, in new_handler
    raise ex
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 698, in _run_job
    result = cmd_copy(params)
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 331, in __call__
    return self.handler(*args, **kwargs)
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\__init__.py", line 816, in default_command_handler
    return op(**command_args)
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\operations\blob.py", line 126, in create_container
    client = blob_data_service_factory(cmd.cli_ctx, kwargs)
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\_client_factory.py", line 84, in blob_data_service_factory
    location_mode=kwargs.pop('location_mode', None))
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\_client_factory.py", line 39, in generic_data_service_factory
    socket_timeout, token_credential, location_mode=location_mode)
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\_client_factory.py", line 32, in get_storage_data_service_client
    location_mode=location_mode)
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\client_factory.py", line 211, in get_data_service_client
    logger.debug('Getting data service client service_type=%s', service_type.__name__)
AttributeError: 'NoneType' object has no attribute '__name__'
  • After:
> az storage container create -n ystesting
The command failed with an unexpected error. Here is the traceback:
No module named 'azure.multiapi.storage'
Traceback (most recent call last):
  File "D:\workspace\ysenv\lib\site-packages\knack\cli.py", line 233, in invoke
    cmd_result = self.invocation.execute(args)
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 664, in execute
    raise ex
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 727, in _run_jobs_serially
    results.append(self._run_job(expanded_arg, cmd_copy))
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 719, in _run_job
    return cmd_copy.exception_handler(ex)
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\__init__.py", line 338, in new_handler
    raise ex
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 698, in _run_job
    result = cmd_copy(params)
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\commands\__init__.py", line 331, in __call__
    return self.handler(*args, **kwargs)
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\__init__.py", line 816, in default_command_handler
    return op(**command_args)
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\operations\blob.py", line 126, in create_container
    client = blob_data_service_factory(cmd.cli_ctx, kwargs)
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\_client_factory.py", line 76, in blob_data_service_factory
    blob_service = get_blob_service_by_type(cli_ctx, blob_type) or get_blob_service_by_type(cli_ctx, 'block')
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\sdkutil.py", line 34, in get_blob_service_by_type
    return type_to_service[blob_type](cli_ctx)
  File "d:\workspace\azure-cli\src\azure-cli\azure\cli\command_modules\storage\sdkutil.py", line 28, in <lambda>
    'block': lambda ctx: get_sdk(ctx, ResourceType.DATA_STORAGE, 'BlockBlobService', mod='blob', checked=False),
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\profiles\__init__.py", line 91, in get_sdk
    return _sdk_get_versioned_sdk(cli_ctx.cloud.profile, resource_type, *attr_args, **kwargs)
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\profiles\_shared.py", line 584, in get_versioned_sdk
    loaded_obj = _get_attr(sdk_path, mod_attr_path, checked)
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\profiles\_shared.py", line 548, in _get_attr
    raise ex
  File "d:\workspace\azure-cli\src\azure-cli-core\azure\cli\core\profiles\_shared.py", line 539, in _get_attr
    op = import_module(full_mod_path)
  File "C:\Users\yishiwang\AppData\Local\Programs\Python\Python37\lib\importlib\__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
ModuleNotFoundError: No module named 'azure.multiapi.storage'

History Notes

[Component Name 1] BREAKING CHANGE: az command a: Make some customer-facing breaking change.
[Component Name 2] az command b: Add some customer-facing feature.


This checklist is used to make sure that common guidelines for a pull request are followed.

@evelyn-ys evelyn-ys self-assigned this Feb 19, 2021
@yonzhan
Copy link
Collaborator

yonzhan commented Feb 19, 2021

Storage

@yonzhan
Copy link
Collaborator

yonzhan commented Feb 19, 2021

@evelyn-ys Good PR description, I like it.

Comment on lines +28 to +30
'block': lambda ctx: get_sdk(ctx, ResourceType.DATA_STORAGE, 'BlockBlobService', mod='blob', checked=False),
'page': lambda ctx: get_sdk(ctx, ResourceType.DATA_STORAGE, 'PageBlobService', mod='blob', checked=False),
'append': lambda ctx: get_sdk(ctx, ResourceType.DATA_STORAGE, 'AppendBlobService', mod='blob', checked=False)
Copy link
Member

@jiasli jiasli Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would really prefer to make checked=False the default for get_sdk, otherwise it is not universal and 'NoneType' object has no attribute '__name__' will continue to happen in other command modules. (#15776 (comment))

Copy link
Member

@jiasli jiasli Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already made checked=False the default for _get_attr in #14690

def _get_attr(sdk_path, mod_attr_path, checked=False):
"""If `checked` is True, None is returned in case of import failure."""

but it only applies to get_client_class which calls _get_attr without checked:

def get_client_class(resource_type):
return _get_attr(resource_type.import_prefix, '#' + resource_type.client_name)

get_versioned_sdk will default checked to True via

def get_versioned_sdk(api_profile, resource_type, *attr_args, **kwargs):
checked = kwargs.get('checked', True)

loaded_obj = _get_attr(sdk_path, mod_attr_path, checked)

Copy link
Member Author

@evelyn-ys evelyn-ys Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I totally agree with you. It's just I was afraid of making great changes to all modules. Should we discuss whether to change the behavior or not with all module owners?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can go ahead and make the change. If CI passes, we are fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python's import statement raises ImportError in case of non-existing module.

Also, checking whether the result is None is against the look before you leap practice.

@evelyn-ys evelyn-ys changed the title [Storage] Fix #14054: 'NoneType' object has no attribute '__name__' [Core] Default get_sdk to raise ImportError when sdk not found Feb 19, 2021

def get_versioned_sdk(api_profile, resource_type, *attr_args, **kwargs):
checked = kwargs.get('checked', True)
checked = kwargs.get('checked', None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still overwrites the default checked=False of _get_attr. It is coincident that False and None have the same effect.

If the default is checked=True, it will be overwritten as None.


def get_versioned_sdk(api_profile, resource_type, *attr_args, **kwargs):
checked = kwargs.get('checked', True)
checked = kwargs.get('checked', None)
Copy link
Member

@jiasli jiasli Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation of get_models is indeed a design flaw that can't distinguish

  • expected import failure caused by incompatible SDK profile (API version)
  • corrupted SDK installation

When the SDK installation is corrupted, this will trigger unclear error message like #14054 in many places:

c.argument('encryption_type', min_api='2019-07-01', arg_type=get_enum_type(self.get_models('EncryptionType')),

Disk, CreationData, DiskCreateOption, Encryption = cmd.get_models(
'Disk', 'CreationData', 'DiskCreateOption', 'Encryption')

A better pattern is to

  1. call get_models lazily only when min_api or max_api is satisfied
  2. error out if import failure happens

This requires large amount of effort inspecting the whole code base.

@evelyn-ys evelyn-ys changed the title [Core] Default get_sdk to raise ImportError when sdk not found [Storage] Fix #14054: 'NoneType' object has no attribute '__name__' Feb 20, 2021
@jiasli jiasli requested a review from qwordy February 20, 2021 03:32
@evelyn-ys evelyn-ys merged commit 109f495 into Azure:dev Feb 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AttributeError: 'NoneType' object has no attribute '__name__'

4 participants