Repository: twisted/pydoctor

Issue:
Regression after 22.2.2  - reparenting confusion crash
When reparenting a class to a module of the same name, then, the next reparent might to move the object to the class instead of moving it to the module. Leading to AssertionError.

Looks like it's a bug introduced by a combinaison trying to comply with mypy in [this commit](https://github.com/twisted/pydoctor/commit/2a0f98e272842ce1c4a78adbba086ef804ef80be) and me changing `ob.contents.get` by `ob.resoveName` to fix #505 

```python
# We should not read:
assert isinstance(old_parent, Module)
# But actually:
assert isinstance(old_parent, CanContainImportsDocumentable)
```


```python
moving 'Wappalyzer.Wappalyzer.WebPage' into 'Wappalyzer'
moving 'Wappalyzer.Wappalyzer.Wappalyzer' into 'Wappalyzer'
moving 'Wappalyzer.Wappalyzer.analyze' into 'Wappalyzer'
Traceback (most recent call last):
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/bin/pydoctor", line 8, in <module>
    sys.exit(main())
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/lib/python3.8/site-packages/pydoctor/driver.py", line 431, in main
    system.process()
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/lib/python3.8/site-packages/pydoctor/model.py", line 1031, in process
    self.processModule(mod)
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/lib/python3.8/site-packages/pydoctor/model.py", line 1016, in processModule
    builder.processModuleAST(ast, mod)
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/lib/python3.8/site-packages/pydoctor/astbuilder.py", line 1[18](https://github.com/chorsley/python-Wappalyzer/runs/5469341311?check_suite_focus=true#step:5:18)2, in processModuleAST
    self.ModuleVistor(self, mod).visit(mod_ast)
  File "/opt/hostedtoolcache/Python/3.8.12/x64/lib/python3.8/ast.py", line 371, in visit
    return visitor(node)
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/lib/python3.8/site-packages/pydoctor/astbuilder.py", line 239, in visit_Module
    self.default(node)
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/lib/python3.8/site-packages/pydoctor/astbuilder.py", line 2[19](https://github.com/chorsley/python-Wappalyzer/runs/5469341311?check_suite_focus=true#step:5:19), in default
    self.visit(child)
  File "/opt/hostedtoolcache/Python/3.8.12/x64/lib/python3.8/ast.py", line 371, in visit
    return visitor(node)
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/lib/python3.8/site-packages/pydoctor/astbuilder.py", line 343, in visit_ImportFrom
    self._importNames(modname, node.names)
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/lib/python3.8/site-packages/pydoctor/astbuilder.py", line 412, in _importNames
    ob.reparent(current, asname)
  File "/home/runner/work/python-Wappalyzer/python-Wappalyzer/.tox/docs/lib/python3.8/site-packages/pydoctor/model.py", line [25](https://github.com/chorsley/python-Wappalyzer/runs/5469341311?check_suite_focus=true#step:5:25)2, in reparent
    assert isinstance(old_parent, Module)
AssertionError
```

I'll add a system test with `python-Wappalyzer`.

Comments:

Patch:
diff --git a/README.rst b/README.rst
index b707e46d..bdbe5848 100644
--- a/README.rst
+++ b/README.rst
@@ -75,6 +75,7 @@ in development
 * Add client side search system based on lunr.js.
 * Fix broken links in docstring summaries.
 * Add cache for the xref linker, reduces the number of identical warnings.
+* Fix crash when reparenting objects with duplicate names.
 
 pydoctor 22.2.2
 ^^^^^^^^^^^^^^^
diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py
index 5242f4cb..594fc2f6 100644
--- a/pydoctor/astbuilder.py
+++ b/pydoctor/astbuilder.py
@@ -397,7 +397,9 @@ class ModuleVistor(ast.NodeVisitor):
 
             # Move re-exported objects into current module.
             if asname in exports and mod is not None:
-                ob = mod.resolveName(orgname)
+                # In case of duplicates names, we can't rely on resolveName,
+                # So we use content.get first to resolve non-alias names. 
+                ob = mod.contents.get(orgname) or mod.resolveName(orgname)
                 if ob is None:
                     self.builder.warning("cannot resolve re-exported name",
                                          f'{modname}.{orgname}')
diff --git a/pydoctor/model.py b/pydoctor/model.py
index b92c2f53..018f7dce 100644
--- a/pydoctor/model.py
+++ b/pydoctor/model.py
@@ -251,7 +251,7 @@ class Documentable:
         # :/
         self._handle_reparenting_pre()
         old_parent = self.parent
-        assert isinstance(old_parent, Module)
+        assert isinstance(old_parent, CanContainImportsDocumentable)
         old_name = self.name
         self.parent = self.parentMod = new_parent
         self.name = new_name


Test patch:
diff --git a/pydoctor/test/test_packages.py b/pydoctor/test/test_packages.py
index 25f6e967..1051c294 100644
--- a/pydoctor/test/test_packages.py
+++ b/pydoctor/test/test_packages.py
@@ -1,5 +1,6 @@
 from pathlib import Path
 from typing import Type
+import pytest
 
 from pydoctor import model
 
@@ -154,3 +155,14 @@ def test_reparenting_follows_aliases() -> None:
         return
     else:
         raise AssertionError("Congratulation!")
+
+@pytest.mark.parametrize('modname', ['reparenting_crash','reparenting_crash_alt'])
+def test_reparenting_crash(modname: str) -> None:
+    """
+    Test for https://github.com/twisted/pydoctor/issues/513
+    """
+    system = processPackage(modname)
+    mod = system.allobjects[modname]
+    assert isinstance(mod.contents[modname], model.Class)
+    assert isinstance(mod.contents['reparented_func'], model.Function)
+    assert isinstance(mod.contents[modname].contents['reparented_func'], model.Function)
diff --git a/pydoctor/test/testpackages/reparenting_crash/__init__.py b/pydoctor/test/testpackages/reparenting_crash/__init__.py
new file mode 100644
index 00000000..d7b9a5bb
--- /dev/null
+++ b/pydoctor/test/testpackages/reparenting_crash/__init__.py
@@ -0,0 +1,3 @@
+from .reparenting_crash import reparenting_crash, reparented_func
+
+__all__ = ['reparenting_crash', 'reparented_func']
\ No newline at end of file
diff --git a/pydoctor/test/testpackages/reparenting_crash/reparenting_crash.py b/pydoctor/test/testpackages/reparenting_crash/reparenting_crash.py
new file mode 100644
index 00000000..e5de4bb0
--- /dev/null
+++ b/pydoctor/test/testpackages/reparenting_crash/reparenting_crash.py
@@ -0,0 +1,8 @@
+
+class reparenting_crash:
+    ...
+    def reparented_func():
+        ...
+
+def reparented_func():
+    ...
\ No newline at end of file
diff --git a/pydoctor/test/testpackages/reparenting_crash_alt/__init__.py b/pydoctor/test/testpackages/reparenting_crash_alt/__init__.py
new file mode 100644
index 00000000..9397062c
--- /dev/null
+++ b/pydoctor/test/testpackages/reparenting_crash_alt/__init__.py
@@ -0,0 +1,3 @@
+from .reparenting_crash_alt import reparenting_crash_alt, reparented_func
+
+__all__ = ['reparenting_crash_alt', 'reparented_func']
diff --git a/pydoctor/test/testpackages/reparenting_crash_alt/_impl.py b/pydoctor/test/testpackages/reparenting_crash_alt/_impl.py
new file mode 100644
index 00000000..fb0a6552
--- /dev/null
+++ b/pydoctor/test/testpackages/reparenting_crash_alt/_impl.py
@@ -0,0 +1,6 @@
+class reparenting_crash_alt:
+    ...
+    def reparented_func():
+        ...
+def reparented_func():
+    ...
diff --git a/pydoctor/test/testpackages/reparenting_crash_alt/reparenting_crash_alt.py b/pydoctor/test/testpackages/reparenting_crash_alt/reparenting_crash_alt.py
new file mode 100644
index 00000000..119fd4ee
--- /dev/null
+++ b/pydoctor/test/testpackages/reparenting_crash_alt/reparenting_crash_alt.py
@@ -0,0 +1,2 @@
+
+from ._impl import reparenting_crash_alt, reparented_func