Repository: Azure/pykusto

Issue:
Refactor array, mapping dynamic types
* Introduce DynamicExpression and AnyExpression
* ArrayExpression and MappingExpression should return AnyExpression
* Delete Column.__getitem__
* Fix inheritance tree

Discussion:

Patch:
diff --git a/pykusto/expressions.py b/pykusto/expressions.py
index d921749..e5ceedc 100644
--- a/pykusto/expressions.py
+++ b/pykusto/expressions.py
@@ -14,7 +14,7 @@ MappingType = Union[Mapping, 'MappingExpression']
 DatetimeType = Union[datetime, 'DatetimeExpression']
 TimespanType = Union[timedelta, 'TimespanExpression']
 AggregationType = Union['AggregationExpression']
-DynamicType = Union[ArrayType, MappingType]
+DynamicType = Union['DynamicExpression']
 OrderType = Union[DatetimeType, TimespanType, NumberType, StringType]
 
 
@@ -421,7 +421,20 @@ class TimespanExpression(BaseExpression):
         )))
 
 
-class ArrayExpression(BaseExpression):
+class ArrayBaseExpression(BaseExpression):
+    def __getitem__(self, index: NumberType) -> 'AnyExpression':
+        return AnyExpression(KQL('{}[{}]'.format(self.kql, _subexpr_to_kql(index))))
+
+
+class MappingBaseExpression(BaseExpression):
+    def __getitem__(self, index: StringType) -> 'AnyExpression':
+        return AnyExpression(KQL('{}[{}]'.format(self.kql, _subexpr_to_kql(index))))
+
+    def __getattr__(self, name: str) -> 'AnyExpression':
+        return AnyExpression(KQL('{}.{}'.format(self.kql, name)))
+
+
+class ArrayExpression(ArrayBaseExpression):
     def __len__(self) -> NumberExpression:
         return self.array_length()
 
@@ -437,16 +450,13 @@ class ArrayExpression(BaseExpression):
             ', '.join('{}'.format(_subexpr_to_kql(e) for e in elements))
         )))
 
-    def __getitem__(self, index: NumberType) -> BaseExpression:
-        return BaseExpression(KQL('{}[{}]'.format(self.kql, _subexpr_to_kql(index))))
-
     def assign_to(self, *columns: 'Column') -> 'AssignmentBase':
         if len(columns) <= 1:
             return super().assign_to(*columns)
         return AssignmentToMultipleColumns(columns, self)
 
 
-class MappingExpression(BaseExpression):
+class MappingExpression(MappingBaseExpression):
     def keys(self) -> ArrayExpression:
         return ArrayExpression(KQL('bag_keys({})'.format(self.kql)))
 
@@ -456,8 +466,18 @@ class MappingExpression(BaseExpression):
             ', '.join('"{}", {}'.format(k, _subexpr_to_kql(v)) for k, v in kwargs)
         )))
 
-    def __getitem__(self, index: StringType) -> BaseExpression:
-        return BaseExpression(KQL('{}[{}]'.format(self.kql, _subexpr_to_kql(index))))
+
+class DynamicExpression(ArrayExpression, MappingExpression):
+    def __getitem__(self, index: Union[StringType, NumberType]) -> 'AnyExpression':
+        return AnyExpression(KQL('{}[{}]'.format(self.kql, _subexpr_to_kql(index))))
+
+
+class AnyExpression(
+    NumberExpression, BooleanExpression,
+    StringExpression, DynamicExpression,
+    DatetimeExpression, TimespanExpression
+):
+    pass
 
 
 class AggregationExpression(BaseExpression):
@@ -545,20 +565,13 @@ class AssignmentFromAggregationToColumn(AssignmentBase):
         super().__init__(None if column is None else column.kql, aggregation)
 
 
-class Column(
-    NumberExpression, BooleanExpression, StringExpression,
-    ArrayExpression, MappingExpression, DatetimeExpression,
-    TimespanExpression
-):
+class Column(AnyExpression):
     _name: str
 
     def __init__(self, name: str) -> None:
         super().__init__(KQL("['{}']".format(name) if '.' in name else name))
         self._name = name
 
-    def __getattr__(self, name: str) -> 'Column':
-        return Column(self._name + '.' + name)
-
     def as_subexpression(self) -> KQL:
         return self.kql
 
@@ -572,10 +585,6 @@ class Column(
             return AssignmentFromColumnToColumn(columns[0], self)
         return ArrayExpression.assign_to(self, *columns)
 
-    def __call__(self, *args, **kwargs):
-        # Someone tried to call a non-existent method, and a column object was generated
-        raise AttributeError("No such method: " + self._name.split('.')[-1])
-
 
 class ColumnGenerator:
     def __getattr__(self, name: str) -> Column:
diff --git a/setup.py b/setup.py
index b31e771..01983a5 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='pykusto',
-    version='0.0.5',
+    version='0.0.6',
     packages=find_packages(exclude=['test']),
     url='https://github.com/Azure/pykusto',
     license='MIT License',


Test patch:
diff --git a/test/test_expressions.py b/test/test_expressions.py
index 7c42e8a..5b6f766 100644
--- a/test/test_expressions.py
+++ b/test/test_expressions.py
@@ -38,22 +38,46 @@ class TestExpressions(TestBase):
             Query().where(col.arr[col.foo * 2] == 'bar').render(),
         )
 
+    def test_array_access_yields_any_expression(self):
+        self.assertEqual(
+            ' | where (cos(arr[3])) < 1',
+            Query().where(col.arr[3].cos() < 1).render(),
+        )
+
     def test_mapping_access(self):
         self.assertEqual(
             ' | where (dict["key"]) == "bar"',
             Query().where(col.dict['key'] == 'bar').render(),
         )
 
+    def test_mapping_access_attribute(self):
+        self.assertEqual(
+            ' | where (dict.key) == "bar"',
+            Query().where(col.dict.key == 'bar').render(),
+        )
+
     def test_mapping_access_expression_index(self):
         self.assertEqual(
             ' | where (dict[foo]) == "bar"',
             Query().where(col.dict[col.foo] == 'bar').render(),
         )
 
+    def test_mapping_access_yields_any_expression(self):
+        self.assertEqual(
+            ' | where (dict["key"]) contains "substr"',
+            Query().where(col.dict['key'].contains("substr")).render(),
+        )
+
+    def test_dynamic(self):
+        self.assertEqual(
+            ' | where (dict["foo"][0].bar[1][2][(tolower(bar))]) > time(1.0:0:0.0)',
+            Query().where(col.dict['foo'][0].bar[1][2][col.bar.lower()] > timedelta(1)).render(),
+        )
+
     def test_assign_to(self):
         self.assertEqual(
-            " | extend ['foo.bar'] = (shoo * 2)",
-            Query().extend((col.shoo * 2).assign_to(col.foo.bar)).render(),
+            " | extend bar = (foo * 2)",
+            Query().extend((col.foo * 2).assign_to(col.bar)).render(),
         )
         self.assertEqual(
             " | extend foo = (shoo * 2)",
@@ -78,19 +102,7 @@ class TestExpressions(TestBase):
             col.foo.is_empty().kql,
         )
 
-    def test_method_does_not_exist(self):
-        self.assertRaises(
-            AttributeError("No such method: non_existant_method"),
-            col.foo.non_existant_method,
-        )
-
     def test_column_generator(self):
-        self.assertEqual(
-            " | project ['foo.bar']",
-            Query().project(col.foo.bar).render(),
-        )
-
-    def test_column_generator_2(self):
         self.assertEqual(
             " | project ['foo.bar']",
             Query().project(col['foo.bar']).render(),