Repository: apache/airflow

Issue:
Cast to string in ds macro functions

As already written in this issue https://github.com/apache/airflow/issues/19241 strptime function required string, but got proxy if the variables ds/next_ds (the types of these variables changed on version 2.2.0) sent.
This change will make the function `ds_add` and `ds_format` backward compatible.

<!--
Thank you for contributing! Please make sure that your code changes
are covered with tests. And in case of new features or big changes
remember to adjust the documentation.

Feel free to ping committers for the review!

In case of existing issue, reference it using one of the following:

closes: #ISSUE
related: #ISSUE

How to write a good git commit message:
http://chris.beams.io/posts/git-commit/
-->

---
**^ Add meaningful description above**

Read the **[Pull Request Guidelines](https://github.com/apache/airflow/blob/main/CONTRIBUTING.rst#pull-request-guidelines)** for more information.
In case of fundamental code change, Airflow Improvement Proposal ([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvements+Proposals)) is needed.
In case of a new dependency, check compliance with the [ASF 3rd Party License Policy](https://www.apache.org/legal/resolved.html#category-x).
In case of backwards incompatible changes please leave a note in [UPDATING.md](https://github.com/apache/airflow/blob/main/UPDATING.md).

next_ds changed to proxy and it cannot be used in ds_add macro function
### Apache Airflow version

2.2.0 (latest released)

### Operating System

Ubuntu

### Versions of Apache Airflow Providers

_No response_

### Deployment

Docker-Compose

### Deployment details

_No response_

### What happened

Tried to use this this code:
`some_variable='{{macros.ds_format(macros.ds_add(next_ds, '
                 '(ti.start_date - ti.execution_date).days), '
                 '"%Y-%m-%d", "%Y-%m-%d 21:00:00")}}')`
but got this error:
`strptime() argument 1 must be str, not Proxy`
because the `next_ds` variable changed to proxy.

### What you expected to happen

_No response_

### How to reproduce

_No response_

### Anything else

_No response_

### Are you willing to submit PR?

- [ ] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)



Discussion:
Can you add tests for this? This should be done by adding a new file at `tests/macros/test_macros.py` with two test functions, one each for calling `ds_add` and `ds_format` with a lazy object proxy.
@ShaharPotash1  - any chance to add the tests?

Thanks for opening your first issue here! Be sure to follow the issue template!

@potiuk 
I think we need to explicitly cast inputs with `str()` in `airflow/macros/__init__.py`. For example:

```python
def ds_add(ds, days):
    ds = datetime.strptime(str(ds), '%Y-%m-%d')  # Note the extra str() call.
    ...
```

Fancy a pull request fixing this? Some tests to make sure this doesn't regress would probably be needed.

Resolving patch:
diff --git a/airflow/macros/__init__.py b/airflow/macros/__init__.py
--- a/airflow/macros/__init__.py
+++ b/airflow/macros/__init__.py
@@ -19,13 +19,17 @@
 import uuid  # noqa
 from datetime import datetime, timedelta
 from random import random  # noqa
+from typing import Any, Optional, Union
 
 import dateutil  # noqa
+import lazy_object_proxy
 
 from airflow.macros import hive  # noqa
 
+TemplateStringInput = Union[str, lazy_object_proxy.Proxy]
 
-def ds_add(ds, days):
+
+def ds_add(ds: TemplateStringInput, days: int) -> str:
     """
     Add or subtract days from a YYYY-MM-DD
 
@@ -39,13 +43,13 @@ def ds_add(ds, days):
     >>> ds_add('2015-01-06', -5)
     '2015-01-01'
     """
-    ds = datetime.strptime(ds, '%Y-%m-%d')
-    if days:
-        ds = ds + timedelta(days)
-    return ds.isoformat()[:10]
+    if not days:
+        return str(ds)
+    dt = datetime.strptime(str(ds), "%Y-%m-%d") + timedelta(days=days)
+    return dt.strftime("%Y-%m-%d")
 
 
-def ds_format(ds, input_format, output_format):
+def ds_format(ds: TemplateStringInput, input_format: str, output_format: str) -> str:
     """
     Takes an input string and outputs another string
     as specified in the output format
@@ -62,10 +66,10 @@ def ds_format(ds, input_format, output_format):
     >>> ds_format('1/5/2015', "%m/%d/%Y",  "%Y-%m-%d")
     '2015-01-05'
     """
-    return datetime.strptime(ds, input_format).strftime(output_format)
+    return datetime.strptime(str(ds), input_format).strftime(output_format)
 
 
-def datetime_diff_for_humans(dt, since=None):
+def datetime_diff_for_humans(dt: Any, since: Optional[datetime] = None) -> str:
     """
     Return a human-readable/approximate difference between two datetimes, or
     one and now.


Test patch:
diff --git a/tests/macros/test_macros.py b/tests/macros/test_macros.py
new file mode 100644
--- /dev/null
+++ b/tests/macros/test_macros.py
@@ -0,0 +1,90 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+import lazy_object_proxy
+import pendulum
+import pytest
+
+from airflow import macros
+from airflow.utils import timezone
+
+
+@pytest.mark.parametrize(
+    "ds, days, expected",
+    [
+        ("2015-01-01", 5, "2015-01-06"),
+        ("2015-01-02", 0, "2015-01-02"),
+        ("2015-01-06", -5, "2015-01-01"),
+        (lazy_object_proxy.Proxy(lambda: "2015-01-01"), 5, "2015-01-06"),
+        (lazy_object_proxy.Proxy(lambda: "2015-01-02"), 0, "2015-01-02"),
+        (lazy_object_proxy.Proxy(lambda: "2015-01-06"), -5, "2015-01-01"),
+    ],
+)
+def test_ds_add(ds, days, expected):
+    result = macros.ds_add(ds, days)
+    assert result == expected
+
+
+@pytest.mark.parametrize(
+    "ds, input_format, output_format, expected",
+    [
+        ("2015-01-02", "%Y-%m-%d", "%m-%d-%y", "01-02-15"),
+        ("2015-01-02", "%Y-%m-%d", "%Y-%m-%d", "2015-01-02"),
+        ("1/5/2015", "%m/%d/%Y", "%m-%d-%y", "01-05-15"),
+        ("1/5/2015", "%m/%d/%Y", "%Y-%m-%d", "2015-01-05"),
+        (lazy_object_proxy.Proxy(lambda: "2015-01-02"), "%Y-%m-%d", "%m-%d-%y", "01-02-15"),
+        (lazy_object_proxy.Proxy(lambda: "2015-01-02"), "%Y-%m-%d", "%Y-%m-%d", "2015-01-02"),
+        (lazy_object_proxy.Proxy(lambda: "1/5/2015"), "%m/%d/%Y", "%m-%d-%y", "01-05-15"),
+        (lazy_object_proxy.Proxy(lambda: "1/5/2015"), "%m/%d/%Y", "%Y-%m-%d", "2015-01-05"),
+    ],
+)
+def test_ds_format(ds, input_format, output_format, expected):
+    result = macros.ds_format(ds, input_format, output_format)
+    assert result == expected
+
+
+@pytest.mark.parametrize(
+    "dt, since, expected",
+    [
+        (
+            timezone.datetime(2017, 1, 2),
+            None,
+            pendulum.instance(timezone.datetime(2017, 1, 2)).diff_for_humans(),
+        ),
+        (timezone.datetime(2017, 1, 2), timezone.datetime(2017, 1, 3), "1 day before"),
+        (timezone.datetime(2017, 1, 2), timezone.datetime(2017, 1, 1), "1 day after"),
+        (
+            lazy_object_proxy.Proxy(lambda: timezone.datetime(2017, 1, 2)),
+            None,
+            pendulum.instance(timezone.datetime(2017, 1, 2)).diff_for_humans(),
+        ),
+        (
+            lazy_object_proxy.Proxy(lambda: timezone.datetime(2017, 1, 2)),
+            timezone.datetime(2017, 1, 3),
+            "1 day before",
+        ),
+        (
+            lazy_object_proxy.Proxy(lambda: timezone.datetime(2017, 1, 2)),
+            timezone.datetime(2017, 1, 1),
+            "1 day after",
+        ),
+    ],
+)
+def test_datetime_diff_for_humans(dt, since, expected):
+    result = macros.datetime_diff_for_humans(dt, since)
+    assert result == expected