Repository: pypa/setuptools

Issue:
Remove site file usage
In #1998 and pypa/virtualenv#1638 and other places, we learned that:

- virtualenv will not be superseded by venv.
- virtualenv 20 still (and more aggressively) uses a site.py file to hack into Python.
- As a result, virtualenv is the only package allowed to use site.py and setuptools needs to drop support for eggs on PYTHONPATH taking precedence over other packages on sys.path or any other features that rely on site.py.

@gaborbernat Does that sound about right?


Discussion:
Sounds about right for python 2. On python 3 virtualenv does not use site.py at all. I'd say site.py is reserved for OS usage though, consider using the site customize file which was designed for such usage.
I see. That's a little more awkward then. That means that setuptools on Python 2.7 is _incompatible_ with virtualenv 20. So users are kinda screwed anyway, since setuptools doesn't declare a dependency on virtualenv and can't really declare an incompatibility.

That means the only way to support virtualenv 20 is to remove this functionality from setuptools 44.x. I'm a little reluctant to do that as it doesn't give a lot of wiggle room to navigate between Python 2 compatibility and virtualenv compatibility. That is, if setuptools removes this functionality in setuptools 44.2, 44.1.1 becomes the last supported version with this functionality (for Python 2). I guess that's okay if no one misses it.

Okay, I think I have a plan:

- Remove this functionality on Setuptools N+1 (Python 3 only).
- Work through any issues reported by users.
- Assuming all issues are relatively workable, backport the removal to Setuptools 44.x.


Resolving patch:
diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py
--- a/setuptools/command/develop.py
+++ b/setuptools/command/develop.py
@@ -139,7 +139,6 @@ def install_for_development(self):
             self.reinitialize_command('build_ext', inplace=1)
             self.run_command('build_ext')
 
-        self.install_site_py()  # ensure that target dir is site-safe
         if setuptools.bootstrap_install_from:
             self.easy_install(setuptools.bootstrap_install_from)
             setuptools.bootstrap_install_from = None
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -205,7 +205,6 @@ def initialize_options(self):
         self.pth_file = self.always_copy_from = None
         self.site_dirs = None
         self.installed_projects = {}
-        self.sitepy_installed = False
         # Always read easy_install options, even if we are subclassed, or have
         # an independent instance created.  This ensures that defaults will
         # always come from the standard configuration file(s)' "easy_install"
@@ -494,12 +493,8 @@ def check_site_dir(self):
         else:
             self.pth_file = None
 
-        if instdir not in map(normalize_path, _pythonpath()):
-            # only PYTHONPATH dirs need a site.py, so pretend it's there
-            self.sitepy_installed = True
-        elif self.multi_version and not os.path.exists(pth_file):
-            self.sitepy_installed = True  # don't need site.py in this case
-            self.pth_file = None  # and don't create a .pth file
+        if self.multi_version and not os.path.exists(pth_file):
+            self.pth_file = None  # don't create a .pth file
         self.install_dir = instdir
 
     __cant_write_msg = textwrap.dedent("""
@@ -656,9 +651,6 @@ def _tmpdir(self):
             os.path.exists(tmpdir) and rmtree(rmtree_safe(tmpdir))
 
     def easy_install(self, spec, deps=False):
-        if not self.editable:
-            self.install_site_py()
-
         with self._tmpdir() as tmpdir:
             if not isinstance(spec, Requirement):
                 if URL_SCHEME(spec):
@@ -1317,38 +1309,6 @@ def byte_compile(self, to_compile):
         Please make the appropriate changes for your system and try again.
         """).strip()
 
-    def install_site_py(self):
-        """Make sure there's a site.py in the target dir, if needed"""
-
-        if self.sitepy_installed:
-            return  # already did it, or don't need to
-
-        sitepy = os.path.join(self.install_dir, "site.py")
-        source = resource_string("setuptools", "site-patch.py")
-        source = source.decode('utf-8')
-        current = ""
-
-        if os.path.exists(sitepy):
-            log.debug("Checking existing site.py in %s", self.install_dir)
-            with io.open(sitepy) as strm:
-                current = strm.read()
-
-            if not current.startswith('def __boot():'):
-                raise DistutilsError(
-                    "%s is not a setuptools-generated site.py; please"
-                    " remove it." % sitepy
-                )
-
-        if current != source:
-            log.info("Creating %s", sitepy)
-            if not self.dry_run:
-                ensure_directory(sitepy)
-                with io.open(sitepy, 'w', encoding='utf-8') as strm:
-                    strm.write(source)
-            self.byte_compile([sitepy])
-
-        self.sitepy_installed = True
-
     def create_home_path(self):
         """Create directories under ~."""
         if not self.user:
diff --git a/setuptools/site-patch.py b/setuptools/site-patch.py
deleted file mode 100644
--- a/setuptools/site-patch.py
+++ /dev/null
@@ -1,76 +0,0 @@
-def __boot():
-    import sys
-    import os
-    PYTHONPATH = os.environ.get('PYTHONPATH')
-    if PYTHONPATH is None or (sys.platform == 'win32' and not PYTHONPATH):
-        PYTHONPATH = []
-    else:
-        PYTHONPATH = PYTHONPATH.split(os.pathsep)
-
-    pic = getattr(sys, 'path_importer_cache', {})
-    stdpath = sys.path[len(PYTHONPATH):]
-    mydir = os.path.dirname(__file__)
-
-    for item in stdpath:
-        if item == mydir or not item:
-            continue  # skip if current dir. on Windows, or my own directory
-        importer = pic.get(item)
-        if importer is not None:
-            loader = importer.find_module('site')
-            if loader is not None:
-                # This should actually reload the current module
-                loader.load_module('site')
-                break
-        else:
-            try:
-                import imp  # Avoid import loop in Python 3
-                stream, path, descr = imp.find_module('site', [item])
-            except ImportError:
-                continue
-            if stream is None:
-                continue
-            try:
-                # This should actually reload the current module
-                imp.load_module('site', stream, path, descr)
-            finally:
-                stream.close()
-            break
-    else:
-        raise ImportError("Couldn't find the real 'site' module")
-
-    # 2.2 comp
-    known_paths = dict([(
-        makepath(item)[1], 1) for item in sys.path])  # noqa
-
-    oldpos = getattr(sys, '__egginsert', 0)  # save old insertion position
-    sys.__egginsert = 0  # and reset the current one
-
-    for item in PYTHONPATH:
-        addsitedir(item)  # noqa
-
-    sys.__egginsert += oldpos  # restore effective old position
-
-    d, nd = makepath(stdpath[0])  # noqa
-    insert_at = None
-    new_path = []
-
-    for item in sys.path:
-        p, np = makepath(item)  # noqa
-
-        if np == nd and insert_at is None:
-            # We've hit the first 'system' path entry, so added entries go here
-            insert_at = len(new_path)
-
-        if np in known_paths or insert_at is None:
-            new_path.append(item)
-        else:
-            # new path after the insert point, back-insert it
-            new_path.insert(insert_at, item)
-            insert_at += 1
-
-    sys.path[:] = new_path
-
-
-if __name__ == 'site':
-    __boot()
-    del __boot


Test patch:
diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -61,14 +61,6 @@ def as_requirement(self):
 
 
 class TestEasyInstallTest:
-    def test_install_site_py(self, tmpdir):
-        dist = Distribution()
-        cmd = ei.easy_install(dist)
-        cmd.sitepy_installed = False
-        cmd.install_dir = str(tmpdir)
-        cmd.install_site_py()
-        assert (tmpdir / 'site.py').exists()
-
     def test_get_script_args(self):
         header = ei.CommandSpec.best().from_environment().as_header()
         expected = header + DALS(r"""