diff --git a/documentation/functions.md b/documentation/functions.md
index 985b9cac..3765d1cd 100644
--- a/documentation/functions.md
+++ b/documentation/functions.md
@@ -28,7 +28,7 @@ Below is the list of every function/operator currently supported in PyDough as a
* [HOUR](#hour)
* [MINUTE](#minute)
* [SECOND](#second)
- * [DATEDIFF] (#datediff)
+ * [DATEDIFF](#datediff)
- [Conditional Functions](#conditional-functions)
* [IFF](#iff)
* [ISIN](#isin)
@@ -54,15 +54,31 @@ Below is the list of every function/operator currently supported in PyDough as a
- [Window Functions](#window-functions)
* [RANKING](#ranking)
* [PERCENTILE](#percentile)
+- [Banned Python Logic](#banned-python-logic)
+ * [\_\_bool\_\_](#__bool__)
+ * [\_\_call\_\_](#call_banned)
+ * [\_\_floor\_\_](#floor_banned)
+ * [\_\_ceil\_\_](#ceil_banned)
+ * [\_\_trunc\_\_](#trunc_banned)
+ * [\_\_reversed\_\_](#reversed_banned)
+ * [\_\_int\_\_](#int_banned)
+ * [\_\_float\_\_](#float_banned)
+ * [\_\_complex\_\_](#complex_banned)
+ * [\_\_index\_\_](#index_banned)
+ * [\_\_len\_\_](#len_banned)
+ * [\_\_contains\_\_](#contains_banned)
+ * [\_\_setitem\_\_](#setitem_banned)
+
## Binary Operators
Below is each binary operator currently supported in PyDough.
+
### Arithmetic
Supported mathematical operations: addition (`+`), subtraction (`-`), multiplication (`*`), division (`/`), exponentiation (`**`).
@@ -75,6 +91,7 @@ Lineitems(value = (extended_price * (1 - (discount ** 2)) + 1.0) / part.retail_p
> The behavior when the denominator is `0` depends on the database being used to evaluate the expression.
+
### Comparisons
Expression values can be compared using standard comparison operators: `<=`, `<`, `==`, `!=`, `>` and `>=`:
@@ -94,6 +111,7 @@ Customers(
> Chained inequalities, like `a <= b <= c`, can cause undefined/incorrect behavior in PyDough. Instead, use expressions like `(a <= b) & (b <= c)`, or the [MONOTONIC](#monotonic) function.
+
### Logical
Multiple boolean expression values can be logically combined with `&`, `|` and `~` being used as logical AND, OR and NOT, respectively:
@@ -113,11 +131,13 @@ Customers(
> Do **NOT** use the builtin Python syntax `and`, `or`, or `not` on PyDough node. Using these instead of `&`, `|` or `~` can result in undefined incorrect results.
+
## Unary Operators
Below is each unary operator currently supported in PyDough.
+
### Negation
A numerical expression's sign can be flipped by prefixing it with the `-` operator:
@@ -127,11 +147,13 @@ Lineitems(lost_value = extended_price * (-discount))
```
+
## Other Operators
Below are all other operators currently supported in PyDough that use other syntax besides function calls:
+
### Slicing
A string expression can have a substring extracted with Python string slicing syntax `s[a:b:c]`:
@@ -147,11 +169,13 @@ Customers(
> PyDough currently only supports combinations of `string[start:stop:step]` where `step` is either 1 or omitted, and both `start` and `stop` are either non-negative values or omitted.
+
## String Functions
Below is each function currently supported in PyDough that operates on strings.
+
### LOWER
Calling `LOWER` on a string converts its characters to lowercase:
@@ -161,6 +185,7 @@ Customers(lowercase_name = LOWER(name))
```
+
### UPPER
Calling `UPPER` on a string converts its characters to uppercase:
@@ -170,6 +195,7 @@ Customers(uppercase_name = UPPER(name))
```
+
### LENGTH
Calling `length` on a string returns the number of characters it contains:
@@ -179,6 +205,7 @@ Suppliers(n_chars_in_comment = LENGTH(comment))
```
+
### STARTSWITH
The `STARTSWITH` function checks if its first argument begins with its second argument as a string prefix:
@@ -188,6 +215,7 @@ Parts(begins_with_yellow = STARTSWITH(name, "yellow"))
```
+
### ENDSWITH
The `ENDSWITH` function checks if its first argument ends with its second argument as a string suffix:
@@ -197,6 +225,7 @@ Parts(ends_with_chocolate = ENDSWITH(name, "chocolate"))
```
+
### CONTAINS
The `CONTAINS` function checks if its first argument contains its second argument as a substring:
@@ -206,6 +235,7 @@ Parts(is_green = CONTAINS(name, "green"))
```
+
### LIKE
The `LIKE` function checks if the first argument matches the SQL pattern text of the second argument, where `_` is a 1 character wildcard and `%` is an 0+ character wildcard.
@@ -217,6 +247,7 @@ Orders(is_special_request = LIKE(comment, "%special%requests%"))
[This link](https://www.w3schools.com/sql/sql_like.asp) explains how these SQL pattern strings work and provides some examples.
+
### JOIN_STRINGS
The `JOIN_STRINGS` function concatenates all its string arguments, using the first argument as a delimiter between each of the following arguments (like the `.join` method in Python):
@@ -230,11 +261,13 @@ Regions.nations.customers(
For instance, `JOIN_STRINGS("; ", "Alpha", "Beta", "Gamma)` returns `"Alpha; Beta; Gamma"`.
+
## Datetime Functions
Below is each function currently supported in PyDough that operates on date/time/timestamp values.
+
### YEAR
Calling `YEAR` on a date/timestamp extracts the year it belongs to:
@@ -244,6 +277,7 @@ Orders.WHERE(YEAR(order_date) == 1995)
```
+
### MONTH
Calling `MONTH` on a date/timestamp extracts the month of the year it belongs to:
@@ -253,6 +287,7 @@ Orders(is_summer = (MONTH(order_date) >= 6) & (MONTH(order_date) <= 8))
```
+
### DAY
Calling `DAY` on a date/timestamp extracts the day of the month it belongs to:
@@ -262,6 +297,7 @@ Orders(is_first_of_month = DAY(order_date) == 1)
```
+
### HOUR
Calling `HOUR` on a date/timestamp extracts the hour it belongs to. The range of output
@@ -272,6 +308,7 @@ Orders(is_12pm = HOUR(order_date) == 12)
```
+
### MINUTE
Calling `MINUTE` on a date/timestamp extracts the minute. The range of output
@@ -282,6 +319,7 @@ Orders(is_half_hour = MINUTE(order_date) == 30)
```
+
### SECOND
Calling `SECOND` on a date/timestamp extracts the second. The range of output
@@ -292,6 +330,7 @@ Orders(is_lt_30_seconds = SECOND(order_date) < 30)
```
+
### DATEDIFF
Calling `DATEDIFF` between 2 timestamps returns the difference in one of `years`, `months`,`days`,`hours`,`minutes` or`seconds`.
@@ -306,7 +345,7 @@ Calling `DATEDIFF` between 2 timestamps returns the difference in one of `years`
```py
# Calculates, for each order, the number of days since January 1st 1992
# that the order was placed:
-orders(
+orders(
days_since=DATEDIFF("days",datetime.date(1992, 1, 1), order_date)
)
```
@@ -323,11 +362,13 @@ The first argument in the `DATEDIFF` function supports the following aliases for
Invalid or unrecognized units will result in an error. For example, `"Days"`, `"DAYS"`, and `"d"` are all treated the same due to case insensitivity.
+
## Conditional Functions
Below is each function currently supported in PyDough that handles conditional logic.
+
### IFF
The `IFF` function cases on the True/False value of its first argument. If it is True, it returns the second argument, otherwise it returns the third argument. In this way, the PyDough code `IFF(a, b, c)` is semantically the same as the SQL expression `CASE WHEN a THEN b ELSE c END`.
@@ -340,6 +381,7 @@ Customers(
```
+
### ISIN
The `ISIN` function takes in an expression and an iterable of literals and returns whether the expression is a member of provided literals.
@@ -349,6 +391,7 @@ Parts.WHERE(ISIN(size, (10, 11, 17, 19, 45)))
```
+
### DEFAULT_TO
The `DEFAULT_TO` function returns the first of its arguments that is non-null (e.g. the same as the `COALESCE` function in SQL):
@@ -358,6 +401,7 @@ Lineitems(adj_tax = DEFAULT_TO(tax, 0))
```
+
### PRESENT
The `PRESENT` function checks if its argument is non-null (e.g. the same as `IS NOT NULL` in SQL):
@@ -367,6 +411,7 @@ Lineitems(has_tax = PRESENT(tax))
```
+
### ABSENT
The `ABSENT` function checks if its argument is null (e.g. the same as `IS NULL` in SQL):
@@ -376,6 +421,7 @@ Lineitems(no_tax = ABSENT(tax))
```
+
### KEEP_IF
The `KEEP_IF` function returns the first function if the second arguments is True, otherwise it returns a null value. In other words, `KEEP_IF(a, b)` is equivalent to the SQL expression `CASE WHEN b THEN a END`.
@@ -385,6 +431,7 @@ TPCH(avg_non_debt_balance = AVG(Customers(no_debt_bal = KEEP_IF(acctbal, acctbal
```
+
### MONOTONIC
The `MONOTONIC` function checks if all of its arguments are in ascending order (e.g. `MONOTONIC(a, b, c, d)` is equivalent to `(a <= b) & (b <= c) & (c <= d)`):
@@ -394,38 +441,56 @@ Lineitems.WHERE(MONOTONIC(10, quantity, 20) & MONOTONIC(5, part.size, 13))
```
+
## Numerical Functions
Below is each numerical function currently supported in PyDough.
+
### ABS
-The `ABS` function returns the absolute value of its input.
+The `ABS` function returns the absolute value of its input. The Python builtin `abs()` function can also be used to accomplish the same thing.
```py
Customers(acct_magnitude = ABS(acctbal))
+# The below statement is equivalent to above.
+Customers(acct_magnitude = abs(acctbal))
```
+
### ROUND
-The `ROUND` function rounds its first argument to the precision of its second argument. The rounding rules used depend on the database's round function.
+The `ROUND` function rounds its first argument to the precision of its second argument. The rounding rules used depend on the database's round function. The Python builtin `round()` function can also be used to accomplish the same thing.
```py
Parts(rounded_price = ROUND(retail_price, 1))
+# The below statement is equivalent to above.
+Parts(rounded_price = round(retail_price, 1))
+```
+
+Note: The default precision for builtin `round` method is 0, to be in alignment with the Python implementation. The PyDough `ROUND` function requires the precision to be specified.
+
+```py
+# This is legal.
+Parts(rounded_price = round(retail_price))
+# This is illegal as precision is not specified.
+Parts(rounded_price = ROUND(retail_price))
```
+
### POWER
-The `POWER` function exponentiates its first argument to the power of its second argument.
+The `POWER` function exponentiates its first argument to the power of its second argument.
```py
Parts(powered_price = POWER(retail_price, 2))
```
+
### SQRT
The `SQRT` function takes the square root of its input. It's equivalent to `POWER(x,0.5)`.
@@ -435,6 +500,7 @@ Parts(sqrt_price = SQRT(retail_price))
```
+
## Aggregation Functions
When terms of a plural sub-collection are accessed, those terms are plural with regards to the current collection. For example, if each nation in `Nations` has multiple `customers`, and each customer has a single `acctbal`, then `customers.acctbal` is plural with regards to `Nations` and cannot be used in any calculations when the current context is `Nations`. The exception to this is when `customers.acctbal` is made singular with regards to `Nations` by aggregating it.
@@ -442,6 +508,7 @@ When terms of a plural sub-collection are accessed, those terms are plural with
Aggregation functions are a special set of functions that, when called on their inputs, convert them from plural to singular. Below is each aggregation function currently supported in PyDough.
+
### SUM
The `SUM` function returns the sum of the plural set of numerical values it is called on.
@@ -451,6 +518,7 @@ Nations(total_consumer_wealth = SUM(customers.acctbal))
```
+
### AVG
The `AVG` function takes the average of the plural set of numerical values it is called on.
@@ -460,6 +528,7 @@ Parts(average_shipment_size = AVG(lines.quantity))
```
+
### MIN
The `MIN` function returns the smallest value from the set of numerical values it is called on.
@@ -469,6 +538,7 @@ Suppliers(cheapest_part_supplied = MIN(supply_records.supply_cost))
```
+
### MAX
The `MAX` function returns the largest value from the set of numerical values it is called on.
@@ -478,6 +548,7 @@ Suppliers(most_expensive_part_supplied = MAX(supply_records.supply_cost))
```
+
### COUNT
The `COUNT` function returns how many non-null records exist on the set of plural values it is called on.
@@ -493,6 +564,7 @@ Nations(num_customers_in_debt = COUNT(customers.WHERE(acctbal < 0)))
```
+
### NDISTINCT
The `NDISTINCT` function returns how many distinct values of its argument exist.
@@ -502,6 +574,7 @@ Customers(num_unique_parts_purchased = NDISTINCT(orders.lines.parts.key))
```
+
### HAS
The `HAS` function is called on a sub-collection and returns `True` if at least one record of the sub-collection exists. In other words, `HAS(x)` is equivalent to `COUNT(x) > 0`.
@@ -511,6 +584,7 @@ Parts.WHERE(HAS(supply_records.supplier.WHERE(nation.name == "GERMANY")))
```
+
### HASNOT
The `HASNOT` function is called on a sub-collection and returns `True` if no records of the sub-collection exist. In other words, `HASNOT(x)` is equivalent to `COUNT(x) == 0`.
@@ -520,11 +594,12 @@ Customers.WHERE(HASNOT(orders))
```
+
## Window Functions
Window functions are special functions whose output depends on other records in the same context.A common example of this is finding the ranking of each record if all of the records were to be sorted.
-Window functions in PyDough have an optional `levels` argument. If this argument is omitted, it means that the window function applies to all records of the current collection (e.g. rank all customers). If it is provided, it should be a value that can be used as an argument to `BACK`, and in that case it means that the set of values used by the window function should be per-record of the correspond ancestor (e.g. rank all customers within each nation).
+Window functions in PyDough have an optional `levels` argument. If this argument is omitted, it means that the window function applies to all records of the current collection (e.g. rank all customers). If it is provided, it should be a value that can be used as an argument to `BACK`, and in that case it means that the set of values used by the window function should be per-record of the correspond ancestor (e.g. rank all customers within each nation).
For example, if using the `RANKING` window function, consider the following examples:
@@ -545,6 +620,7 @@ Regions.nations.customers(r=RANKING(..., levels=3))
Below is each window function currently supported in PyDough.
+
### RANKING
The `RANKING` function returns ordinal position of the current record when all records in the current context are sorted by certain ordering keys. The arguments:
@@ -565,6 +641,7 @@ Customers.orders.WHERE(RANKING(by=order_date.DESC(), levels=1, allow_ties=True)
```
+
### PERCENTILE
The `PERCENTILE` function returns what index the current record belongs to if all records in the current context are ordered then split into evenly sized buckets. The arguments:
@@ -580,3 +657,191 @@ Customers.WHERE(PERCENTILE(by=acctbal.ASC(), n_buckets=1000) == 1000)
# For every region, find the top 5% of customers with the highest account balances.
Regions.nations.customers.WHERE(PERCENTILE(by=acctbal.ASC(), levels=2) > 95)
```
+
+## Banned Python Logic
+
+Below is a list of banned python logic (magic methods,etc.) that are not supported in PyDough. Calling these methods will result in an Exception.
+
+
+
+### \_\_bool\_\_
+
+The `__bool__` magic method is not supported in PyDough. PyDough code cannot be treated as booleans.
+
+```py
+# Not allowed - will raise PyDoughUnqualifiedException
+if Customer and Order:
+ print("Available")
+
+Customers.WHERE((acctbal > 0) and (nation.name == "GERMANY"))
+# Use &`instead of `and`:
+# Customers.WHERE((acctbal > 0) & (nation.name == "GERMANY"))
+
+Orders.WHERE((discount > 0.05) or (tax > 0.08))
+# Use `|` instead of `or`
+# Orders.WHERE((discount > 0.05) | (tax > 0.08))
+
+Parts.WHERE(not(retail_price > 1000))
+# Use `~` instead of `not`
+# Parts.WHERE(~(retail_price > 1000))
+```
+
+
+
+### \_\_call\_\_
+
+The `__call__` magic method is not supported in PyDough as it calls PyDough code as if it were a function.
+
+```py
+# Not allowed - calls PyDough code as if it were a function
+(1 - discount)(extended_price * 0.5)
+```
+
+
+
+### \_\_floor\_\_
+
+The `math.floor` function calls the `__floor__` magic method, which is currently not supported in PyDough.
+
+```py
+Customer(age=math.floor(order.total_price))
+```
+
+
+
+### \_\_ceil\_\_
+
+The `math.ceil` function calls the `__ceil__` magic method, which is currently not supported in PyDough.
+
+```py
+# Not allowed currently- will raise PyDoughUnqualifiedException
+Customer(age=math.ceil(order.total_price))
+```
+
+
+
+### \_\_trunc\_\_
+
+The `math.trunc` function calls the `__trunc__` magic method, which is currently not supported in PyDough.
+
+```py
+# Not allowed currently- will raise PyDoughUnqualifiedException
+Customer(age=math.trunc(order.total_price))
+```
+
+
+
+### \_\_reversed\_\_
+
+The `reversed` function calls the `__reversed__` magic method, which is currently not supported in PyDough.
+
+```py
+# Not allowed currently- will raise PyDoughUnqualifiedException
+Regions(backwards_name=reversed(name))
+```
+
+
+
+### \_\_int\_\_
+
+Casting to `int` calls the `__int__` magic method, which is not supported in PyDough. This operation is not allowed because the implementation has to return an integer instead of a PyDough object.
+
+```py
+# Not allowed currently as it would need to return an int instead of a PyDough object
+Orders(limit=int(order.total_price))
+```
+
+
+
+### \_\_float\_\_
+
+Casting to `float` calls the `__float__` magic method, which is not supported in PyDough. This operation is not allowed because the implementation has to return a float instead of a PyDough object.
+
+```py
+# Not allowed currently as it would need to return a float instead of a PyDough object
+Orders(limit=float(order.quantity))
+```
+
+
+
+### \_\_complex\_\_
+
+Casting to `complex` calls the `__complex__` magic method, which is not supported in PyDough. This operation is not allowed because the implementation has to return a complex instead of a PyDough object.
+
+```py
+# Not allowed currently as it would need to return a complex instead of a PyDough object
+Orders(limit=complex(order.total_price))
+```
+
+
+
+### \_\_index\_\_
+
+Using an object as an index calls the `__index__` magic method, which is not supported in PyDough. This operation is not allowed because the implementation has to return an integer instead of a PyDough object.
+
+```py
+# Not allowed currently as it would need to return an int instead of a PyDough object
+Orders(s="ABCDE"[:order_priority])
+```
+
+
+
+### \_\_nonzero\_\_
+
+Using an object in a boolean context calls the `__nonzero__` magic method, which is not supported in PyDough. This operation is not allowed because the implementation has to return an integer instead of a PyDough object.
+
+```py
+# Not allowed currently as it would need to return an int instead of a PyDough object
+Lineitems(is_taxed=bool(tax))
+```
+
+
+
+### \_\_len\_\_
+
+The `len` function calls the `__len__` magic method, which is not supported in PyDough. This operation is not allowed because the implementation has to return an integer instead of a PyDough object. Instead, usage of [LENGTH](#length) function is recommended.
+
+```py
+# Not allowed currently as it would need to return an int instead of a PyDough object
+Customers(len(customer.name))
+```
+
+
+
+### \_\_contains\_\_
+
+Using the `in` operator calls the `__contains__` magic method, which is not supported in PyDough. This operation is not allowed because the implementation has to return a boolean instead of a PyDough object. Instead, If you need to check if a string is inside another substring, use [CONTAINS](#contains). If you need to check if an expression is a member of a list of literals, use [ISIN](#isin).
+
+```py
+# Not allowed currently as it would need to return a boolean instead of a PyDough object
+Orders('discount' in order.details)
+```
+
+
+
+### \_\_setitem\_\_
+
+Assigning to an index calls the `__setitem__` magic method, which is not supported in PyDough. This operation is not allowed.
+
+```py
+# Not allowed currently as PyDough objects cannot support item assignment.
+Order.details['discount'] = True
+```
+
+
+
+### \_\_iter\_\_
+
+Iterating over an object calls the `__iter__` magic method, which is not supported in PyDough. This operation is not allowed because the implementation has to return an iterator instead of a PyDough object.
+
+```py
+# Not allowed currently as implementation has to return an iterator instead of a PyDough object.
+for item in customer:
+ print(item)
+
+[item for item in customer]
+
+list(customer)
+
+tuple(customer)
+```
diff --git a/pydough/unqualified/unqualified_node.py b/pydough/unqualified/unqualified_node.py
index 2a55a3a2..e22b8bfe 100644
--- a/pydough/unqualified/unqualified_node.py
+++ b/pydough/unqualified/unqualified_node.py
@@ -124,6 +124,10 @@ def __getitem__(self, key):
args: MutableSequence[UnqualifiedNode] = [self]
for arg in (key.start, key.stop, key.step):
coerced_elem = UnqualifiedNode.coerce_to_unqualified(arg)
+ if not isinstance(coerced_elem, UnqualifiedLiteral):
+ raise PyDoughUnqualifiedException(
+ "PyDough objects are currently not supported to be used as indices in Python slices."
+ )
args.append(coerced_elem)
return UnqualifiedOperation("SLICE", args)
else:
@@ -260,6 +264,67 @@ def __call__(self, *args, **kwargs: dict[str, object]):
calc_args.append((name, self.coerce_to_unqualified(arg)))
return UnqualifiedCalc(self, calc_args)
+ def __abs__(self):
+ return UnqualifiedOperation("ABS", [self])
+
+ def __round__(self, n=None):
+ if n is None:
+ n = 0
+ n_unqualified = self.coerce_to_unqualified(n)
+ return UnqualifiedOperation("ROUND", [self, n_unqualified])
+
+ def __floor__(self):
+ raise PyDoughUnqualifiedException(
+ "PyDough does not support the math.floor function at this time."
+ )
+
+ def __ceil__(self):
+ raise PyDoughUnqualifiedException(
+ "PyDough does not support the math.ceil function at this time."
+ )
+
+ def __trunc__(self):
+ raise PyDoughUnqualifiedException(
+ "PyDough does not support the math.trunc function at this time."
+ )
+
+ def __reversed__(self):
+ raise PyDoughUnqualifiedException(
+ "PyDough does not support the reversed function at this time."
+ )
+
+ def __int__(self):
+ raise PyDoughUnqualifiedException("PyDough objects cannot be cast to int.")
+
+ def __float__(self):
+ raise PyDoughUnqualifiedException("PyDough objects cannot be cast to float.")
+
+ def __complex__(self):
+ raise PyDoughUnqualifiedException("PyDough objects cannot be cast to complex.")
+
+ def __index__(self):
+ raise PyDoughUnqualifiedException(
+ "PyDough objects cannot be used as indices in Python slices."
+ )
+
+ def __nonzero__(self):
+ return self.__bool__()
+
+ def __len__(self):
+ raise PyDoughUnqualifiedException(
+ "PyDough objects cannot be used with the len function."
+ )
+
+ def __contains__(self, item):
+ raise PyDoughUnqualifiedException(
+ "PyDough objects cannot be used with the 'in' operator."
+ )
+
+ def __setitem__(self, key, value):
+ raise PyDoughUnqualifiedException(
+ "PyDough objects cannot support item assignment."
+ )
+
def WHERE(self, cond: object) -> "UnqualifiedWhere":
cond_unqualified: UnqualifiedNode = self.coerce_to_unqualified(cond)
return UnqualifiedWhere(self, cond_unqualified)
diff --git a/tests/bad_pydough_functions.py b/tests/bad_pydough_functions.py
index a6a8c2b2..47d4a244 100644
--- a/tests/bad_pydough_functions.py
+++ b/tests/bad_pydough_functions.py
@@ -2,6 +2,8 @@
# mypy: ignore-errors
# ruff & mypy should not try to typecheck or verify any of this
+import math
+
def bad_bool_1():
# Using `or`
@@ -73,3 +75,71 @@ def bad_slice_3():
def bad_slice_4():
# Unsupported slicing: reversed
return Customers(name[::-1])
+
+
+def bad_floor():
+ # Using `math.floor` (calls __floor__)
+ return Customer(age=math.floor(order.total_price))
+
+
+def bad_ceil():
+ # Using `math.ceil` (calls __ceil__)
+ return Customer(age=math.ceil(order.total_price))
+
+
+def bad_trunc():
+ # Using `math.trunc` (calls __trunc__)
+ return Customer(age=math.trunc(order.total_price))
+
+
+def bad_reversed():
+ # Using `reversed` (calls __reversed__)
+ return Regions(backwards_name=reversed(name))
+
+
+def bad_int():
+ # Casting to int (calls __int__)
+ return Orders(limit=int(order.total_price))
+
+
+def bad_float():
+ # Casting to float (calls __float__)
+ return Orders(limit=float(order.quantity))
+
+
+def bad_complex():
+ # Casting to complex (calls __complex__)
+ return Orders(limit=complex(order.total_price))
+
+
+def bad_index():
+ # Using as an index (calls __index__)
+ return Orders(s="ABCDE"[:order_priority])
+
+
+def bad_nonzero():
+ # Using in a boolean context (calls __nonzero__)
+ return Lineitems(is_taxed=bool(tax))
+
+
+def bad_len():
+ # Using `len` (calls __len__)
+ return Customers(len(customer.name))
+
+
+def bad_contains():
+ # Using `in` operator (calls __contains__)
+ return Orders("discount" in order.details)
+
+
+def bad_setitem():
+ # Assigning to an index (calls __setitem__)
+ order.details["discount"] = True
+ return order
+
+
+def bad_iter():
+ # Iterating over an object (calls __iter__)
+ for item in customer:
+ print(item)
+ return customer
diff --git a/tests/simple_pydough_functions.py b/tests/simple_pydough_functions.py
index f96e7061..f099fd50 100644
--- a/tests/simple_pydough_functions.py
+++ b/tests/simple_pydough_functions.py
@@ -391,6 +391,10 @@ def annotated_assignment():
return Nations.WHERE(region.name == chosen_region)
+def abs_round_magic_method():
+ return DailyPrices(abs_low=abs(low), round_low=round(low, 2), round_zero=round(low))
+
+
def years_months_days_hours_datediff():
y1_datetime = datetime.datetime(2025, 5, 2, 11, 00, 0)
return Transactions.WHERE((YEAR(date_time) < 2025))(
diff --git a/tests/test_unqualified_node.py b/tests/test_unqualified_node.py
index 386266b2..c5de0648 100644
--- a/tests/test_unqualified_node.py
+++ b/tests/test_unqualified_node.py
@@ -12,6 +12,19 @@
bad_bool_1,
bad_bool_2,
bad_bool_3,
+ bad_ceil,
+ bad_complex,
+ bad_contains,
+ bad_float,
+ bad_floor,
+ bad_index,
+ bad_int,
+ bad_iter,
+ bad_len,
+ bad_nonzero,
+ bad_reversed,
+ bad_setitem,
+ bad_trunc,
bad_window_1,
bad_window_2,
bad_window_3,
@@ -21,6 +34,7 @@
bad_window_7,
)
from simple_pydough_functions import (
+ abs_round_magic_method,
annotated_assignment,
args_kwargs,
class_handling,
@@ -504,6 +518,11 @@ def test_unqualified_to_string(
"?.Nations.WHERE((?.region.name == 'SOUTH WEST AMERICA'))",
id="annotated_assignment",
),
+ pytest.param(
+ abs_round_magic_method,
+ "?.DailyPrices(abs_low=ABS(?.low), round_low=ROUND(?.low, 2), round_zero=ROUND(?.low, 0))",
+ id="abs_round_magic_method",
+ ),
],
)
def test_init_pydough_context(
@@ -580,6 +599,71 @@ def test_init_pydough_context(
"`n_buckets` argument must be a positive integer",
id="bad_window_7",
),
+ pytest.param(
+ bad_floor,
+ "PyDough does not support the math.floor function at this time.",
+ id="bad_floor",
+ ),
+ pytest.param(
+ bad_ceil,
+ "PyDough does not support the math.ceil function at this time.",
+ id="bad_ceil",
+ ),
+ pytest.param(
+ bad_trunc,
+ "PyDough does not support the math.trunc function at this time.",
+ id="bad_trunc",
+ ),
+ pytest.param(
+ bad_reversed,
+ "PyDough does not support the reversed function at this time.",
+ id="bad_reversed",
+ ),
+ pytest.param(
+ bad_int,
+ "PyDough objects cannot be cast to int.",
+ id="bad_int",
+ ),
+ pytest.param(
+ bad_float,
+ "PyDough objects cannot be cast to float.",
+ id="bad_float",
+ ),
+ pytest.param(
+ bad_complex,
+ "PyDough objects cannot be cast to complex.",
+ id="bad_complex",
+ ),
+ pytest.param(
+ bad_index,
+ "PyDough objects cannot be used as indices in Python slices.",
+ id="bad_index",
+ ),
+ pytest.param(
+ bad_nonzero,
+ "PyDough code cannot be treated as a boolean. If you intend to do a logical operation, use `|`, `&` and `~` instead of `or`, `and` and `not`.",
+ id="bad_nonzero_1",
+ ),
+ pytest.param(
+ bad_len,
+ "PyDough objects cannot be used with the len function.",
+ id="bad_len",
+ ),
+ pytest.param(
+ bad_contains,
+ "PyDough objects cannot be used with the 'in' operator.",
+ id="bad_contains",
+ ),
+ pytest.param(
+ bad_setitem,
+ "PyDough objects cannot support item assignment.",
+ id="bad_setitem",
+ ),
+ pytest.param(
+ bad_iter,
+ "Cannot index into PyDough object \?.customer with 0",
+ id="bad_iter",
+ ),
],
)
def test_unqualified_errors(