Repository: pyinstaller/pyinstaller

Issue:
filename.py conflicts with filename/__init__.py
When compiling for **Windows 10** with `pyinstaller filename.py`, and having a sub-module coincidentally named `filename` with an `__init__.py`, the compiler will treat the starting point to be `__init__.py` instead of `filename.py`. The issue doesn't occur with other OS'es.
Package/module name match -> no error message
I've run into the issue described in bug [#2049](https://github.com/pyinstaller/pyinstaller/issues/2049#issue-160286511), i.e. a package and the initial module 'scriptname' have the same name (excepting the .py suffix). If the package __init__.py is empty (or absent) there is no relevant error message when the pyinstaller runs, or when the program runs, it simply does nothing. If there is a print statement in the __init__.py the error message is not helpful:
```
Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
Python runtime state: core initialized
ModuleNotFoundError: No module named 'encodings'
```
Although the discussion for bug 2049 suggests this situation is 'not permitted' in python, the ambiguity never comes up as the module is the entry point and is never **itself** called.

Found with: pyinstaller 4.10, Windows 10, python 3.7-9


Proposed solutions:
- An error message would save substantial time when someone stumbles across this issue
- In the specific case of the entry point & module matching either name could be automatically renamed


Example to reproduce:
matching_name.py:
```
print("Test point one")
from matching_name import call_module

if __name__ == '__main__':
    call_module.InnerClass()
```

matching_name/call_module.py:
```
class InnerClass(object):
    def __init__(self):
        print("Inner init test point")
```

pyinstaller matching_name.py


Discussion:
@mightymercado thanks for letting us know.
Confirmed. This is a valid issue. I'll work on a fix.
Not just on Windows--I just ran into this exact issue on Ubuntu 16.
@shoshber this is an issue with ModuleGraph and Python imports. I'm looking into a fix.
Hrmm, well I can't even run your code under normal Python...
```python
> python matching_name.py
Test point one
Test point one
Traceback (most recent call last):
  File "/e/notebooks/pyi-issues/6702/matching_name.py", line 2, in <module>
    from matching_name import call_module
  File "/e/notebooks/pyi-issues/6702/matching_name.py", line 2, in <module>
    from matching_name import call_module
ImportError: cannot import name 'call_module' from partially initialized module 'matching_name' (most likely due to a circular import) (/e/notebooks/pyi-issues/6702/matching_name.py)
```
... so I'd be pretty reluctant to try and make PyInstaller magic a functional application out of it. I wouldn't be opposed to some kind of warning if a module name is ambiguous but I don't see how PyInstaller can do any more than that.

Incidentally, I get exactly the same error when ran through PyInstaller.
```python
> ./matching_name 
Test point one
Test point one
Traceback (most recent call last):
  File "matching_name.py", line 2, in <module>
  File "/io/PyInstaller/loader/pyimod03_importers.py", line 495, in exec_module
  File "matching_name.py", line 2, in <module>
ImportError: cannot import name 'call_module' from partially initialized module 'matching_name' (most likely due to a circular import) (/e/notebooks/pyi-issues/6702/dist/matching_name/matching_name.pyc)
[195227] Failed to execute script 'matching_name' due to unhandled exception!
```

Not sure how you got that encodings error - that usually implies you've moved the executable out of a onedir build and ran it without the rest of the bundle. 
> `ImportError: cannot import name 'call_module' from partially initialized module 'matching_name' (most likely due to a circular import) (/e/notebooks/pyi-issues/6702/matching_name.py)`

This happens if the package is a namespace package. If it has an `__init__.py`, then the sample code does work in python.
That said, the situation with entry-point script shadowing the package name (or vice versa, depending on your perspective) is the same as if you tried to run the entry point as `python -m matching_name` instead of `python matching_name.py`. If there was no package named `matching_name`, the script (now treated as module) would run correctly, but if the package shadows the module, you'll get an error.

So I don't think it's unreasonable to consider this corner case as a situation that should be avoided on user's part, instead of trying to accommodate it.
That's quite reasonable, the primary motivator was that the code ran fine directly in my development but the pyinstaller executable runs and does nothing (e.g. no print test point output). Without some clue as to the source this is quite tricky to debug, so even a message related to the fact that the entry point is empty (as presumably the .exe is pointing at the `__init__.py`) would be helpful.

I agree it is obviously not a common situation however, and perhaps python itself should be explicitly failing or warning when this sort of shadowing happens. Also, this will teach me not to be lazy in naming, even when the entry point is essentially a stub for the module.
I suppose the specific case of the provided entry point script being replaced with another module/package of the same name is detectable. We can compare the path of the entry point script to the path of the module that `modulegraph` picks up and raise an error if they don't match. Producing a functional program out of that case still sounds impossible to me.
I suppose detecting this situation and raising an error should be enough.

Resolving patch:
diff --git a/PyInstaller/building/utils.py b/PyInstaller/building/utils.py
--- a/PyInstaller/building/utils.py
+++ b/PyInstaller/building/utils.py
@@ -17,7 +17,6 @@
 import hashlib
 import os
 import pathlib
-import pkgutil
 import platform
 import shutil
 import struct
@@ -586,45 +585,11 @@ def format_binaries_and_datas(binaries_or_datas, workingdir=None):
     return toc_datas
 
 
-def _load_code(modname, filename):
-    path_item = os.path.dirname(filename)
-    if os.path.basename(filename).startswith('__init__.py'):
-        # this is a package
-        path_item = os.path.dirname(path_item)
-    if os.path.basename(path_item) == '__pycache__':
-        path_item = os.path.dirname(path_item)
-    importer = pkgutil.get_importer(path_item)
-    package, _, modname = modname.rpartition('.')
-
-    if hasattr(importer, 'find_loader'):
-        loader, portions = importer.find_loader(modname)
-    else:
-        loader = importer.find_module(modname)
-
-    logger.debug('Compiling %s', filename)
-    if loader and hasattr(loader, 'get_code'):
-        return loader.get_code(modname)
-    else:
-        # Just as ``python foo.bar`` will read and execute statements in ``foo.bar``,  even though it lacks the ``.py``
-        # extension, so ``pyinstaller foo.bar``  should also work. However, Python's import machinery doesn't load files
-        # without a ``.py`` extension. So, use ``compile`` instead.
-        #
-        # On a side note, neither the Python 2 nor Python 3 calls to ``pkgutil`` and ``find_module`` above handle
-        # modules ending in ``.pyw``, even though ``imp.find_module`` and ``import <name>`` both work. This code
-        # supports ``.pyw`` files.
-
-        # Open the source file in binary mode and allow the `compile()` call to detect the source encoding.
-        with open(filename, 'rb') as f:
-            source = f.read()
-        return compile(source, filename, 'exec')
-
-
 def get_code_object(modname, filename):
     """
     Get the code-object for a module.
 
-    This is a extra-simple version for compiling a module. It is not worth spending more effort here, as it is only
-    used in the rare case if outXX-Analysis.toc exists, but outXX-PYZ.toc does not.
+    This is a simplifed non-performant version which circumvents __pycache__.
     """
 
     try:
@@ -636,10 +601,9 @@ def get_code_object(modname, filename):
             return compile(txt, filename, 'exec')
         else:
             logger.debug('Compiling %s', filename)
-            co = _load_code(modname, filename)
-            if not co:
-                raise ValueError("Module file %s is missing" % filename)
-            return co
+            with open(filename, 'rb') as f:
+                source = f.read()
+            return compile(source, filename, 'exec')
     except SyntaxError as e:
         print("Syntax error in ", filename)
         print(e.args)


Test patch:
diff --git a/tests/functional/data/name_clash_with_entry_point/matching_name.py b/tests/functional/data/name_clash_with_entry_point/matching_name.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/data/name_clash_with_entry_point/matching_name.py
@@ -0,0 +1,2 @@
+print("Running matching_name.py as", __name__)
+from matching_name import submodule
diff --git a/tests/functional/data/name_clash_with_entry_point/matching_name/__init__.py b/tests/functional/data/name_clash_with_entry_point/matching_name/__init__.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/data/name_clash_with_entry_point/matching_name/__init__.py
@@ -0,0 +1 @@
+print("Running matching_name/__init__.py as", __name__)
diff --git a/tests/functional/data/name_clash_with_entry_point/matching_name/submodule.py b/tests/functional/data/name_clash_with_entry_point/matching_name/submodule.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/data/name_clash_with_entry_point/matching_name/submodule.py
@@ -0,0 +1 @@
+print("Running matching_name/submodule.py as", __name__)
diff --git a/tests/functional/test_basic.py b/tests/functional/test_basic.py
--- a/tests/functional/test_basic.py
+++ b/tests/functional/test_basic.py
@@ -14,6 +14,9 @@
 import os
 import sys
 import shutil
+from pathlib import Path
+import subprocess
+import re
 
 import pytest
 
@@ -797,3 +800,27 @@ def test_subprocess_in_windowed_mode(pyi_windowed_builder):
         """.format(repr(sys.executable)),
         pyi_args=["--windowed"]
     )
+
+
+def test_package_entry_point_name_collision(pyi_builder):
+    """
+    Check when an imported package has the same name as the entry point script. Despite the obvious ambiguity, Python
+    still handles this case fine and PyInstaller should too.
+    """
+    script = Path(__file__, "../data/name_clash_with_entry_point/matching_name.py").resolve()
+
+    # Each file prints its filename and the value of __name__.
+    expected = [
+        ('matching_name.py', '__main__'),
+        ('matching_name/__init__.py', 'matching_name'),
+        ('matching_name/submodule.py', 'matching_name.submodule'),
+    ]
+
+    # Include a verification that unfrozen Python does still work.
+    p = subprocess.run([sys.executable, str(script)], stdout=subprocess.PIPE, universal_newlines=True)
+    assert re.findall("Running (.*) as (.*)", p.stdout) == expected
+
+    pyi_builder.test_script(str(script))
+    exe, = pyi_builder._find_executables("matching_name")
+    p = subprocess.run([exe], stdout=subprocess.PIPE, universal_newlines=True)
+    assert re.findall("Running (.*) as (.*)", p.stdout) == expected