Repository: arrow-py/arrow

Issue:
arrow.get(datetime.now()).humanize() == "6 hours ago"
# Issue Description

It looks like `arrow` is doing something weird timezone information when operating `datetime` objects, including `datetime` objects returned by e.g. `dateparser.parse('now')`.

What is the appropriate incantation to get `arrow` and `datetime` to agree?  I've managed to do it in one of my projects by using epochseconds, but would prefer to keep my dates as either `datetime` or `arrow` object representations.

I am in central standard time, so the "6 hours ago" seems to reflect my TZ offset from UTC, but even providing a `..., tzinfo=` does not appear to do the correct thing.

Overall, the mixture of simple things that don't work is overwhelming, and there should maybe be some examples in the documentation for how to do this right (e.g. setting tzinfo on the datetime object makes arrow's object correct, but setting tzinfo on the arrow object does not).

## date(1) vs datetime

As best I can tell, `datetime.now()` is correct, and lacks a specific timezone.

```py
>>> import datetime, subprocess
>>> datetime.datetime.now()
datetime.datetime(2021, 3, 9, 19, 55, 36, 539945)
>>> subprocess.check_output('date', text=True)
'Tue Mar  9 19:55:39 CST 2021\n'
```

## datetime vs arrow

This jives with the 6 hour time difference from UTC, but why does it say "6 hours ago"?  Neither arrow nor datetime were provided with a timezone, so this seems odd.

```py
>>> import arrow, datetime
>>> arrow.get(datetime.datetime.now()).humanize()
'6 hours ago'
```

#### datetime with tzinfo

This happens even with a custom timezone, unless the timezone is specified on the datetime object.

```py
>>> import arrow, dateparser, dateutil, datetime
>>> tz = dateutil.tz.tzlocal()

# With the local timezone on arrow.get() does NOT work
>>> arrow.get(datetime.datetime.now(), tzinfo=tz).humanize()
'6 hours ago'

# With the local timezone on datetime.now() DOES work
>>> arrow.get(datetime.datetime.now(tz=tz)).humanize()
'just now'

# With a custom timezone (set to be GMT-6)
>>> mytz = dateutil.tz.tzoffset('MyTZ', datetime.timedelta(hours=6))
>>> mytz
tzoffset('MyTZ', 21600)
>>> arrow.get(datetime.datetime.now(), tzinfo=mytz).humanize()
'6 hours ago'
```

## dateparser

Per the documentation:

> dateparser relies on following libraries in some ways:
> • tzlocal to reliably get local timezone.

However, this doesn't seem to work correctly either.

```py
>>> import arrow, dateparser
>>> arrow.get(dateparser.parse('now')).humanize()
'6 hours ago'
```

### dateparser with tzinfo

This happens regardless of whether `tzinfo` is specified.

```py
>>> import arrow, dateparser, dateutil
>>> tz = dateutil.tz.tzlocal()
>>> tz
tzlocal()
>>> arrow.get(dateparser.parse('now'), tzinfo=tz).humanize()
'6 hours ago'
```

## Unix Timestamps

It looks like there's an issue with timestamps as well.  If I pass the timestamp directly to `arrow.get()`, it returns the correct value.

```py
>>> dt = datetime.datetime.fromtimestamp(1615341484)
>>> dt
datetime.datetime(2021, 3, 9, 19, 58, 4)
>>> !date
Tue Mar  9 20:02:38 CST 2021

# We can see that arrow is correct in its humanize()
>>> arrow.get(1615341484).humanize()
'4 minutes ago'

# However, using the datetime derived from the timestamp exhibits the same issue
>>> arrow.get(dt).humanize()
'6 hours ago'
```


## System Info

```
$ sw_vers
ProductName:	macOS
ProductVersion:	11.3
BuildVersion:	20E5172h
```

```
$ python3 --version
Python 3.8.2
```

```
$ python3 -m pip freeze | grep arrow
arrow==1.0.3
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/arrow-py/arrow/pull/968?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py) Report
> Merging [#968](https://codecov.io/gh/arrow-py/arrow/pull/968?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py) (bce4f54) into [master](https://codecov.io/gh/arrow-py/arrow/commit/08705bcde7a8eef65a40f43acd733863d8c1f63b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py) (08705bc) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/arrow-py/arrow/pull/968/graphs/tree.svg?width=650&height=150&src=pr&token=4EkGnFsIGn&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py)](https://codecov.io/gh/arrow-py/arrow/pull/968?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py)

```diff
@@           Coverage Diff           @@
##           master     #968   +/-   ##
=======================================
  Coverage   99.75%   99.75%           
=======================================
  Files          10       10           
  Lines        2014     2014           
  Branches      323      323           
=======================================
  Hits         2009     2009           
  Misses          4        4           
  Partials        1        1           
```


| [Impacted Files](https://codecov.io/gh/arrow-py/arrow/pull/968?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py) | Coverage Δ | |
|---|---|---|
| [arrow/factory.py](https://codecov.io/gh/arrow-py/arrow/pull/968/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py#diff-YXJyb3cvZmFjdG9yeS5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/arrow-py/arrow/pull/968?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/arrow-py/arrow/pull/968?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py). Last update [08705bc...bce4f54](https://codecov.io/gh/arrow-py/arrow/pull/968?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=arrow-py).

jadchaar: LGTM, but I think this is a more widespread issue @systemcatch. We should be passing along the tz object in all of these other factory methods as well: https://github.com/arrow-py/arrow/blob/08705bcde7a8eef65a40f43acd733863d8c1f63b/arrow/factory.py#L234-L247 (and maybe more!). Weird this has not come up until now, but we may need to apply this change to a bunch of factory methods. Thoughts?
systemcatch: Hey @jadchaar I agree with you on the `Arrow` one to avoid this bug below.

```python
>>> arw=arrow.Arrow(2021, 4, 29, 22, 7, tzinfo="America/Chicago")
>>> arw
<Arrow [2021-04-29T22:07:00-05:00]>
>>> arrow.get(arw, tzinfo="Europe/London")
<Arrow [2021-04-29T22:07:00-05:00]>
```

However the `date` factory doesn't need it I think and all the others seem correct.
jadchaar: @systemcatch I made a few tweaks to comments and made the tzinfo kwarg explicit for the `get()` factory method in 4d65c00eb3f0b5f9a5ef29b5495980d6514c6154. Let me know what you think!
systemcatch: Looks good to me Jad, feel free to merge if you're happy with everything.

Patch:
diff --git a/arrow/factory.py b/arrow/factory.py
index 27dea5c..5787dcf 100644
--- a/arrow/factory.py
+++ b/arrow/factory.py
@@ -205,14 +205,14 @@ class ArrowFactory:
         if len(kwargs) == 1 and tz is None:
             arg_count = 3
 
-        # () -> now, @ utc.
+        # () -> now, @ tzinfo or utc
         if arg_count == 0:
             if isinstance(tz, str):
                 tz = parser.TzinfoParser.parse(tz)
-                return self.type.now(tz)
+                return self.type.now(tzinfo=tz)
 
             if isinstance(tz, dt_tzinfo):
-                return self.type.now(tz)
+                return self.type.now(tzinfo=tz)
 
             return self.type.utcnow()
 
@@ -223,42 +223,42 @@ class ArrowFactory:
             if arg is None:
                 raise TypeError("Cannot parse argument of type None.")
 
-            # try (int, float) -> from timestamp with tz
+            # try (int, float) -> from timestamp @ tzinfo
             elif not isinstance(arg, str) and is_timestamp(arg):
                 if tz is None:
                     # set to UTC by default
                     tz = dateutil_tz.tzutc()
                 return self.type.fromtimestamp(arg, tzinfo=tz)
 
-            # (Arrow) -> from the object's datetime.
+            # (Arrow) -> from the object's datetime @ tzinfo
             elif isinstance(arg, Arrow):
-                return self.type.fromdatetime(arg.datetime)
+                return self.type.fromdatetime(arg.datetime, tzinfo=tz)
 
-            # (datetime) -> from datetime.
+            # (datetime) -> from datetime @ tzinfo
             elif isinstance(arg, datetime):
-                return self.type.fromdatetime(arg)
+                return self.type.fromdatetime(arg, tzinfo=tz)
 
-            # (date) -> from date.
+            # (date) -> from date @ tzinfo
             elif isinstance(arg, date):
-                return self.type.fromdate(arg)
+                return self.type.fromdate(arg, tzinfo=tz)
 
-            # (tzinfo) -> now, @ tzinfo.
+            # (tzinfo) -> now @ tzinfo
             elif isinstance(arg, dt_tzinfo):
-                return self.type.now(arg)
+                return self.type.now(tzinfo=arg)
 
-            # (str) -> parse.
+            # (str) -> parse @ tzinfo
             elif isinstance(arg, str):
                 dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace)
-                return self.type.fromdatetime(dt, tz)
+                return self.type.fromdatetime(dt, tzinfo=tz)
 
             # (struct_time) -> from struct_time
             elif isinstance(arg, struct_time):
                 return self.type.utcfromtimestamp(calendar.timegm(arg))
 
-            # (iso calendar) -> convert then from date
+            # (iso calendar) -> convert then from date @ tzinfo
             elif isinstance(arg, tuple) and len(arg) == 3:
                 d = iso_to_gregorian(*arg)
-                return self.type.fromdate(d)
+                return self.type.fromdate(d, tzinfo=tz)
 
             else:
                 raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.")
@@ -269,9 +269,9 @@ class ArrowFactory:
 
             if isinstance(arg_1, datetime):
 
-                # (datetime, tzinfo/str) -> fromdatetime replace tzinfo.
+                # (datetime, tzinfo/str) -> fromdatetime @ tzinfo
                 if isinstance(arg_2, (dt_tzinfo, str)):
-                    return self.type.fromdatetime(arg_1, arg_2)
+                    return self.type.fromdatetime(arg_1, tzinfo=arg_2)
                 else:
                     raise TypeError(
                         f"Cannot parse two arguments of types 'datetime', {type(arg_2)!r}."
@@ -279,7 +279,7 @@ class ArrowFactory:
 
             elif isinstance(arg_1, date):
 
-                # (date, tzinfo/str) -> fromdate replace tzinfo.
+                # (date, tzinfo/str) -> fromdate @ tzinfo
                 if isinstance(arg_2, (dt_tzinfo, str)):
                     return self.type.fromdate(arg_1, tzinfo=arg_2)
                 else:
@@ -287,7 +287,7 @@ class ArrowFactory:
                         f"Cannot parse two arguments of types 'date', {type(arg_2)!r}."
                     )
 
-            # (str, format) -> parse.
+            # (str, format) -> parse @ tzinfo
             elif isinstance(arg_1, str) and isinstance(arg_2, (str, list)):
                 dt = parser.DateTimeParser(locale).parse(
                     args[0], args[1], normalize_whitespace
@@ -299,7 +299,7 @@ class ArrowFactory:
                     f"Cannot parse two arguments of types {type(arg_1)!r} and {type(arg_2)!r}."
                 )
 
-        # 3+ args -> datetime-like via constructor.
+        # 3+ args -> datetime-like via constructor
         else:
             return self.type(*args, **kwargs)
 


Test patch:
diff --git a/tests/test_factory.py b/tests/test_factory.py
index 5e0020d..4bb81e8 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -4,6 +4,7 @@ from datetime import date, datetime
 import pytest
 from dateutil import tz
 
+from arrow import Arrow
 from arrow.parser import ParserError
 
 from .utils import assert_datetime_equality
@@ -172,6 +173,46 @@ class TestGet:
             2013, 5, 5, 12, 30, 45, 123456, tzinfo=tz.tzutc()
         )
 
+    # regression test for #944
+    def test_one_arg_datetime_tzinfo_kwarg(self):
+
+        dt = datetime(2021, 4, 29, 6)
+
+        result = self.factory.get(dt, tzinfo="America/Chicago")
+
+        expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago"))
+
+        assert_datetime_equality(result._datetime, expected)
+
+    def test_one_arg_arrow_tzinfo_kwarg(self):
+
+        arw = Arrow(2021, 4, 29, 6)
+
+        result = self.factory.get(arw, tzinfo="America/Chicago")
+
+        expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago"))
+
+        assert_datetime_equality(result._datetime, expected)
+
+    def test_one_arg_date_tzinfo_kwarg(self):
+
+        da = date(2021, 4, 29)
+
+        result = self.factory.get(da, tzinfo="America/Chicago")
+
+        expected = Arrow(2021, 4, 29, tzinfo=tz.gettz("America/Chicago"))
+
+        assert result.date() == expected.date()
+        assert result.tzinfo == expected.tzinfo
+
+    def test_one_arg_iso_calendar_tzinfo_kwarg(self):
+
+        result = self.factory.get((2004, 1, 7), tzinfo="America/Chicago")
+
+        expected = Arrow(2004, 1, 4, tzinfo="America/Chicago")
+
+        assert_datetime_equality(result, expected)
+
     def test_one_arg_iso_str(self):
 
         dt = datetime.utcnow()