Repository: iterative/dvc

Issue:
RecursionError from Repo('.').graph()[0].reverse()
**Please provide information about your setup**
DVC version(i.e. `dvc --version`), Platform and method of installation (pip, homebrew, pkg Mac, exe (Windows), DEB(Linux), RPM(Linux))

```
dvc --version
> 0.51.1
```
Installed via pip, on Ubuntu 16.04.6 LTS.

The code:
```
from dvc.repo import Repo
Repo('.').graph()[0].reverse()
```
fails with the following error:

```
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-7-90086e6e0e64> in <module>
----> 1 r.graph()[0].reverse()

~/miniconda3/envs/py36_v11/lib/python3.6/site-packages/networkx/classes/digraph.py in reverse(self, copy)
   1195             H = self.__class__()
   1196             H.graph.update(deepcopy(self.graph))
-> 1197             H.add_nodes_from((n, deepcopy(d)) for n, d in self.node.items())
   1198             H.add_edges_from((v, u, deepcopy(d)) for u, v, d
   1199                              in self.edges(data=True))

~/miniconda3/envs/py36_v11/lib/python3.6/site-packages/networkx/classes/digraph.py in add_nodes_from(self, nodes_for_adding, **attr)
    470 
    471         """
--> 472         for n in nodes_for_adding:
    473             # keep all this inside try/except because
    474             # CPython throws TypeError on n not in self._succ,

~/miniconda3/envs/py36_v11/lib/python3.6/site-packages/networkx/classes/digraph.py in <genexpr>(.0)
   1195             H = self.__class__()
   1196             H.graph.update(deepcopy(self.graph))
-> 1197             H.add_nodes_from((n, deepcopy(d)) for n, d in self.node.items())
   1198             H.add_edges_from((v, u, deepcopy(d)) for u, v, d
   1199                              in self.edges(data=True))

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    148     copier = _deepcopy_dispatch.get(cls)
    149     if copier:
--> 150         y = copier(x, memo)
    151     else:
    152         try:

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _deepcopy_dict(x, memo, deepcopy)
    238     memo[id(x)] = y
    239     for key, value in x.items():
--> 240         y[deepcopy(key, memo)] = deepcopy(value, memo)
    241     return y
    242 d[dict] = _deepcopy_dict

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    178                     y = x
    179                 else:
--> 180                     y = _reconstruct(x, memo, *rv)
    181 
    182     # If is its own copy, don't memoize.

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    278     if state is not None:
    279         if deep:
--> 280             state = deepcopy(state, memo)
    281         if hasattr(y, '__setstate__'):
    282             y.__setstate__(state)

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    148     copier = _deepcopy_dispatch.get(cls)
    149     if copier:
--> 150         y = copier(x, memo)
    151     else:
    152         try:

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _deepcopy_dict(x, memo, deepcopy)
    238     memo[id(x)] = y
    239     for key, value in x.items():
--> 240         y[deepcopy(key, memo)] = deepcopy(value, memo)
    241     return y
    242 d[dict] = _deepcopy_dict

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    178                     y = x
    179                 else:
--> 180                     y = _reconstruct(x, memo, *rv)
    181 
    182     # If is its own copy, don't memoize.

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    278     if state is not None:
    279         if deep:
--> 280             state = deepcopy(state, memo)
    281         if hasattr(y, '__setstate__'):
    282             y.__setstate__(state)

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    148     copier = _deepcopy_dispatch.get(cls)
    149     if copier:
--> 150         y = copier(x, memo)
    151     else:
    152         try:

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _deepcopy_dict(x, memo, deepcopy)
    238     memo[id(x)] = y
    239     for key, value in x.items():
--> 240         y[deepcopy(key, memo)] = deepcopy(value, memo)
    241     return y
    242 d[dict] = _deepcopy_dict

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    178                     y = x
    179                 else:
--> 180                     y = _reconstruct(x, memo, *rv)
    181 
    182     # If is its own copy, don't memoize.

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    278     if state is not None:
    279         if deep:
--> 280             state = deepcopy(state, memo)
    281         if hasattr(y, '__setstate__'):
    282             y.__setstate__(state)

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    148     copier = _deepcopy_dispatch.get(cls)
    149     if copier:
--> 150         y = copier(x, memo)
    151     else:
    152         try:

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _deepcopy_dict(x, memo, deepcopy)
    238     memo[id(x)] = y
    239     for key, value in x.items():
--> 240         y[deepcopy(key, memo)] = deepcopy(value, memo)
    241     return y
    242 d[dict] = _deepcopy_dict

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    178                     y = x
    179                 else:
--> 180                     y = _reconstruct(x, memo, *rv)
    181 
    182     # If is its own copy, don't memoize.

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    278     if state is not None:
    279         if deep:
--> 280             state = deepcopy(state, memo)
    281         if hasattr(y, '__setstate__'):
    282             y.__setstate__(state)

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    148     copier = _deepcopy_dispatch.get(cls)
    149     if copier:
--> 150         y = copier(x, memo)
    151     else:
    152         try:

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _deepcopy_dict(x, memo, deepcopy)
    238     memo[id(x)] = y
    239     for key, value in x.items():
--> 240         y[deepcopy(key, memo)] = deepcopy(value, memo)
    241     return y
    242 d[dict] = _deepcopy_dict

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    178                     y = x
    179                 else:
--> 180                     y = _reconstruct(x, memo, *rv)
    181 
    182     # If is its own copy, don't memoize.

~/miniconda3/envs/py36_v11/lib/python3.6/copy.py in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    279         if deep:
    280             state = deepcopy(state, memo)
--> 281         if hasattr(y, '__setstate__'):
    282             y.__setstate__(state)
    283         else:

~/miniconda3/envs/py36_v11/lib/python3.6/site-packages/dvc/path_info.py in __getattr__(self, name)
    185 
    186     def __getattr__(self, name):
--> 187         return getattr(self.parsed, name)
    188 
    189     @cached_property

... last 1 frames repeated, from the frame below ...

~/miniconda3/envs/py36_v11/lib/python3.6/site-packages/dvc/path_info.py in __getattr__(self, name)
    185 
    186     def __getattr__(self, name):
--> 187         return getattr(self.parsed, name)
    188 
    189     @cached_property

RecursionError: maximum recursion depth exceeded
```



Discussion:

Patch:
diff --git a/dvc/path_info.py b/dvc/path_info.py
index c905f47fe..c7d0051d8 100644
--- a/dvc/path_info.py
+++ b/dvc/path_info.py
@@ -184,6 +184,12 @@ class URLInfo(object):
     __truediv__ = __div__
 
     def __getattr__(self, name):
+        # When deepcopy is called, it creates and object without __init__,
+        # self.parsed is not initialized and it causes infinite recursion.
+        # More on this special casing here:
+        # https://stackoverflow.com/a/47300262/298182
+        if name.startswith("__"):
+            raise AttributeError(name)
         return getattr(self.parsed, name)
 
     @cached_property


Test patch:
diff --git a/tests/unit/test_path_info.py b/tests/unit/test_path_info.py
index 46bd032b9..9ec73389b 100644
--- a/tests/unit/test_path_info.py
+++ b/tests/unit/test_path_info.py
@@ -1,4 +1,5 @@
 import pytest
+import copy
 
 from dvc.path_info import URLInfo, CloudURLInfo
 
@@ -34,3 +35,10 @@ def test_url_info_parents(cls):
         cls("ssh://user@test.com/test1"),
         cls("ssh://user@test.com/"),
     ]
+
+
+@pytest.mark.parametrize("cls", [URLInfo, CloudURLInfo])
+def test_url_info_deepcopy(cls):
+    u1 = cls("ssh://user@test.com:/test1/test2/test3")
+    u2 = copy.deepcopy(u1)
+    assert u1 == u2