Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

{Core} aaz: Improve shorthand syntax #23268

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 26 additions & 41 deletions src/azure-cli-core/azure/cli/core/aaz/_arg_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
# pylint: disable=protected-access

import os
import re
from argparse import Action
from collections import OrderedDict

from knack.log import get_logger

from azure.cli.core import azclierror
from ._base import AAZUndefined
from ._base import AAZUndefined, AAZBlankArgValue
from ._help import AAZShowHelp
from ._utils import AAZShortHandSyntaxParser
from .exceptions import AAZInvalidShorthandSyntaxError, AAZInvalidValueError
Expand Down Expand Up @@ -94,9 +93,7 @@ def setup_operations(cls, dest_ops, values, prefix_keys=None):
if prefix_keys is None:
prefix_keys = []
if values is None:
if cls._schema._blank == AAZUndefined:
raise AAZInvalidValueError("argument cannot be blank")
data = cls._schema._blank # use blank data when values string is None
data = AAZBlankArgValue # use blank data when values string is None
else:
if isinstance(values, list):
assert prefix_keys # the values will be input as an list when parse singular option of a list argument
Expand All @@ -112,11 +109,16 @@ def setup_operations(cls, dest_ops, values, prefix_keys=None):
raise aaz_help
else:
data = values
data = cls.format_data(data)
data = cls.format_data(data)
dest_ops.add(data, *prefix_keys)

@classmethod
def format_data(cls, data):
if data == AAZBlankArgValue:
if cls._schema._blank == AAZUndefined:
raise AAZInvalidValueError("argument value cannot be blank")
data = cls._schema._blank

if isinstance(data, str):
# transfer string into correct data
if cls._schema.enum:
Expand All @@ -136,10 +138,6 @@ def format_data(cls, data):

class AAZCompoundTypeArgAction(AAZArgAction): # pylint: disable=abstract-method

key_pattern = re.compile(
r'^(((\[-?[0-9]+])|(([a-zA-Z0-9_\-]+)(\[-?[0-9]+])?))(\.([a-zA-Z0-9_\-]+)(\[-?[0-9]+])?)*)=(.*)$'
) # 'Partial Value' format

@classmethod
def setup_operations(cls, dest_ops, values, prefix_keys=None):
if prefix_keys is None:
Expand All @@ -161,38 +159,10 @@ def setup_operations(cls, dest_ops, values, prefix_keys=None):
@classmethod
def decode_values(cls, values):
for v in values:
key, key_parts, v = cls._split_value_str(v)
key, key_parts, v = cls._str_parser.split_partial_value(v)
v = cls._decode_value(key, key_parts, v)
yield key, key_parts, v

@classmethod
def _split_value_str(cls, v):
""" split 'Partial Value' format """
assert isinstance(v, str)
match = cls.key_pattern.fullmatch(v)
if not match:
key = None
else:
key = match[1]
v = match[len(match.regs) - 1]
key_parts = cls._split_key(key)
return key, key_parts, v

@staticmethod
def _split_key(key):
""" split index key of 'Partial Value' format """
if key is None:
return tuple()
key_items = []
key = key[0] + key[1:].replace('[', '.[') # transform 'ab[2]' to 'ab.[2]', keep '[1]' unchanged
for part in key.split('.'):
assert part
if part.startswith('['):
assert part.endswith(']')
part = int(part[1:-1])
key_items.append(part)
return tuple(key_items)

@classmethod
def _decode_value(cls, key, key_items, value): # pylint: disable=unused-argument
from ._arg import AAZSimpleTypeArg
Expand All @@ -204,7 +174,7 @@ def _decode_value(cls, key, key_items, value): # pylint: disable=unused-argumen

if len(value) == 0:
# the express "a=" will return the blank value of schema 'a'
return schema._blank
return AAZBlankArgValue

try:
if isinstance(schema, AAZSimpleTypeArg):
Expand Down Expand Up @@ -236,6 +206,11 @@ class AAZObjectArgAction(AAZCompoundTypeArgAction):

@classmethod
def format_data(cls, data):
if data == AAZBlankArgValue:
if cls._schema._blank == AAZUndefined:
raise AAZInvalidValueError("argument value cannot be blank")
data = cls._schema._blank

if data is None:
if cls._schema._nullable:
return data
Expand All @@ -258,6 +233,11 @@ class AAZDictArgAction(AAZCompoundTypeArgAction):

@classmethod
def format_data(cls, data):
if data == AAZBlankArgValue:
if cls._schema._blank == AAZUndefined:
raise AAZInvalidValueError("argument value cannot be blank")
data = cls._schema._blank

if data is None:
if cls._schema._nullable:
return data
Expand Down Expand Up @@ -327,7 +307,7 @@ def setup_operations(cls, dest_ops, values, prefix_keys=None):
# --args [val1,val2,val3]

for value in values:
key, _, _ = cls._split_value_str(value)
key, _, _ = cls._str_parser.split_partial_value(value)
if key is not None:
# key should always be None
raise ex
Expand Down Expand Up @@ -357,6 +337,11 @@ def setup_operations(cls, dest_ops, values, prefix_keys=None):

@classmethod
def format_data(cls, data):
if data == AAZBlankArgValue:
if cls._schema._blank == AAZUndefined:
raise AAZInvalidValueError("argument value cannot be blank")
data = cls._schema._blank

if data is None:
if cls._schema._nullable:
return data
Expand Down
40 changes: 40 additions & 0 deletions src/azure-cli-core/azure/cli/core/aaz/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,43 @@ def build(cls, schema):

def __init__(self, data):
self.data = data


class _AAZBlankArgValueType:
"""Internal class for AAZUndefined global const"""

def __str__(self):
return 'BlankArgValue'

def __repr__(self):
return 'BlankArgValue'

def __eq__(self, other):
return self is other

def __ne__(self, other):
return self is not other

def __bool__(self):
return False

def __lt__(self, other):
self._cmp_err(other, '<')

def __gt__(self, other):
self._cmp_err(other, '>')

def __le__(self, other):
self._cmp_err(other, '<=')

def __ge__(self, other):
self._cmp_err(other, '>=')

def _cmp_err(self, other, op):
raise TypeError(f"unorderable types: {self.__class__.__name__}() {op} {other.__class__.__name__}()")


# AAZ framework defines a global const called AAZUndefined. Which is used to show a field is not defined.
# In order to different with `None` value
# This value is used in aaz package only.
AAZBlankArgValue = _AAZBlankArgValueType()
111 changes: 96 additions & 15 deletions src/azure-cli-core/azure/cli/core/aaz/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import re
from collections import OrderedDict

from azure.cli.core.aaz.exceptions import AAZInvalidShorthandSyntaxError
from ._help import AAZShowHelp
from ._base import AAZBlankArgValue


class AAZShortHandSyntaxParser:

NULL_EXPRESSIONS = ('null',) # user can use "null" string to pass `None` value
HELP_EXPRESSIONS = ('??', ) # the mark to show detail help.

partial_value_key_pattern = re.compile(
r"^(((\[-?[0-9]+])|((([a-zA-Z0-9_\-]+)|('([^']*)'(/([^']*)')*))(\[-?[0-9]+])?))(\.(([a-zA-Z0-9_\-]+)|('([^']*)'(/([^']*)')*))(\[-?[0-9]+])?)*)=(.*)$" # pylint: disable=line-too-long
) # 'Partial Value' format

def __call__(self, data, is_simple=False):
assert isinstance(data, str)
if len(data) == 0:
Expand Down Expand Up @@ -78,21 +83,24 @@ def parse_dict(self, remain): # pylint: disable=too-many-statements
idx += length
if idx < len(remain) and remain[idx] == ':':
idx += 1
if idx >= len(remain):
raise AAZInvalidShorthandSyntaxError(remain, idx, 1, "Cannot parse empty")

try:
value, length = self.parse_value(remain[idx:])
except AAZInvalidShorthandSyntaxError as ex:
ex.error_data = remain
ex.error_at += idx
raise ex
except AAZShowHelp as aaz_help:
aaz_help.keys = [key, *aaz_help.keys]
raise aaz_help
elif idx < len(remain) and remain[idx] in (',', '}'):
# use blank value
value = AAZBlankArgValue
length = 0
else:
raise AAZInvalidShorthandSyntaxError(remain, idx, 1, "Expect character ':'")

if idx >= len(remain):
raise AAZInvalidShorthandSyntaxError(remain, idx, 1, "Cannot parse empty")

try:
value, length = self.parse_value(remain[idx:])
except AAZInvalidShorthandSyntaxError as ex:
ex.error_data = remain
ex.error_at += idx
raise ex
except AAZShowHelp as aaz_help:
aaz_help.keys = [key, *aaz_help.keys]
raise aaz_help
raise AAZInvalidShorthandSyntaxError(remain, idx, 1, "Expect characters ':' or ','")

result[key] = value
idx += length
Expand Down Expand Up @@ -201,3 +209,76 @@ def parse_single_quotes_string(remain):
if quote is not None:
raise AAZInvalidShorthandSyntaxError(remain, idx, 1, f"Miss end quota character: {quote}")
return result, idx

@classmethod
def split_partial_value(cls, v):
""" split 'Partial Value' format """
assert isinstance(v, str)
match = cls.partial_value_key_pattern.fullmatch(v)
if not match:
key = None
else:
key = match[1]
v = match[len(match.regs) - 1]
key_parts = cls.parse_partial_value_key(key)
return key, key_parts, v

@classmethod
def parse_partial_value_key(cls, key):
if key is None:
return tuple()
key_items = []
idx = 0
while idx < len(key):
if key[idx] == '[':
try:
key_item, length = cls.parse_partial_value_idx_key(key[idx:])
except AAZInvalidShorthandSyntaxError as ex:
ex.error_data = key
ex.error_at += idx
raise ex
idx += length
else:
try:
key_item, length = cls.parse_partial_value_prop_key(key[idx:])
except AAZInvalidShorthandSyntaxError as ex:
ex.error_data = key
ex.error_at += idx
raise ex
idx += length
key_items.append(key_item)
return tuple(key_items)

@classmethod
def parse_partial_value_idx_key(cls, remain):
assert remain[0] == '['
idx = 1
while idx < len(remain) and remain[idx] != ']':
idx += 1
if idx < len(remain) and remain[idx] == ']':
result = remain[1:idx]
idx += 1
else:
raise AAZInvalidShorthandSyntaxError(remain, idx, 1, "Expect character ']'")

if len(result) == 0:
raise AAZInvalidShorthandSyntaxError(remain, 0, 2, "Miss index")
if idx < len(remain) and remain[idx] == '.':
idx += 1
return int(result), idx

@classmethod
def parse_partial_value_prop_key(cls, remain):
idx = 0
if remain[0] == "'":
result, length = cls.parse_single_quotes_string(remain)
idx += length
else:
while idx < len(remain) and remain[idx] not in ('.', '['):
idx += 1
result = remain[:idx]
if len(result) == 0:
raise AAZInvalidShorthandSyntaxError(remain, 0, idx, "Miss prop name")
if idx < len(remain) and remain[idx] == '.':
idx += 1
return result, idx
Loading