Skip to content

Commit

Permalink
{Core} aaz: Improve shorthand syntax (#23268)
Browse files Browse the repository at this point in the history
* support blank expression in 'full value' format

* 1.support partial value split in AAZShortHandSyntaxParser 2.support Single Quotes String in partial value keys

* fix style issue
  • Loading branch information
kairu-ms authored Jul 20, 2022
1 parent f31628b commit 0d8b4ab
Show file tree
Hide file tree
Showing 4 changed files with 536 additions and 354 deletions.
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

0 comments on commit 0d8b4ab

Please sign in to comment.