Repository: pypa/virtualenv
Pull request #2640 (2023-09-07 18:29:53)

Fix brew cannot copy
Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>



Patch:
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 46469927..119970e9 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -33,6 +33,8 @@ jobs:
           - macos-latest
           - windows-latest
         include:
+          - { os: macos-latest, py: "brew@3.11" }
+          - { os: macos-latest, py: "brew@3.10" }
           - { os: macos-latest, py: "brew@3.9" }
           - { os: macos-latest, py: "brew@3.8" }
     steps:
@@ -55,22 +57,21 @@ jobs:
         uses: actions/setup-python@v4
         with:
           python-version: "3.11"
-      - name: Install tox
-        run: python -m pip install tox pip -U
-      - uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-      - name: Use local virtualenv for tox
-        run: python -m pip install .
       - name: Setup brew python for test ${{ matrix.py }}
         if: startsWith(matrix.py,'brew@')
         run: |
           set -e
           PY=$(echo '${{ matrix.py }}' | cut -c 6-)
-          brew upgrade python@$PY || brew install python@$PY
-          brew doctor  # Fix dyld library not loaded issues
+          brew cleanup && brew upgrade python@$PY || brew install python@$PY
           echo "/usr/local/opt/python@$PY/libexec/bin" >>"${GITHUB_PATH}"
         shell: bash
+      - name: Install tox
+        run: python -m pip install tox pip -U
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      - name: Use local virtualenv for tox
+        run: python -m pip install .
       - name: Setup python for test ${{ matrix.py }}
         if: "!( startsWith(matrix.py,'brew@') || endsWith(matrix.py, '-dev') )"
         uses: actions/setup-python@v4
@@ -85,7 +86,7 @@ jobs:
               file_handler.write(env)
         shell: python
       - name: Setup test suite
-        run: tox -vv --notest
+        run: tox -vv --notest --skip-missing-interpreters false
       - name: Run test suite
         run: tox --skip-pkg-install
         env:
@@ -122,7 +123,9 @@ jobs:
           python-version: "3.11"
       - name: Install tox
         run: python -m pip install tox
+      - name: Setup check
+        run: python -m tox --skip-missing-interpreters false -e ${{ matrix.tox_env }} --notest
       - name: Run check for ${{ matrix.tox_env }}
-        run: python -m tox -e ${{ matrix.tox_env }}
+        run: python -m tox  --skip-pkg-install -e ${{ matrix.tox_env }}
         env:
           UPGRADE_ADVISORY: "yes"
diff --git a/docs/changelog/2640.bugfix.rst b/docs/changelog/2640.bugfix.rst
new file mode 100644
index 00000000..cd9c3716
--- /dev/null
+++ b/docs/changelog/2640.bugfix.rst
@@ -0,0 +1,1 @@
+Brew on macOS no longer allows copy builds - disallow choosing this by :user:`gaborbernat`.
diff --git a/docs/installation.rst b/docs/installation.rst
index ab582034..fd80f4e4 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -4,7 +4,7 @@ Installation
 via pipx
 --------
 
-:pypi:`virtualenv` is a CLI tool that needs a Python interpreter to run. If you already have a ``Python 3.5+``
+:pypi:`virtualenv` is a CLI tool that needs a Python interpreter to run. If you already have a ``Python 3.7+``
 interpreter the best is to use :pypi:`pipx` to install virtualenv into an isolated environment. This has the added
 benefit that later you'll be able to upgrade virtualenv without affecting other parts of the system.
 
@@ -58,11 +58,14 @@ with a python interpreter:
 
 The root level zipapp is always the current latest release. To get the last supported zipapp against a given python
 minor release use the link ``https://bootstrap.pypa.io/virtualenv/x.y/virtualenv.pyz``, e.g. for the last virtualenv
-supporting Python 2.7 use
-`https://bootstrap.pypa.io/virtualenv/2.7/virtualenv.pyz <https://bootstrap.pypa.io/virtualenv/2.7/virtualenv.pyz>`_.
+supporting Python 3.11 use
+`https://bootstrap.pypa.io/virtualenv/3.11/virtualenv.pyz <https://bootstrap.pypa.io/virtualenv/3.11/virtualenv.pyz>`_.
 
 If you are looking for past version of virtualenv.pyz they are available here:
-https://github.com/pypa/get-virtualenv/blob/<virtualenv version>/public/<python version>/virtualenv.pyz?raw=true
+
+.. code-block:: console
+
+    https://github.com/pypa/get-virtualenv/blob/<virtualenv version>/public/<python version>/virtualenv.pyz?raw=true
 
 latest unreleased
 -----------------
@@ -81,8 +84,8 @@ Python and OS Compatibility
 
 virtualenv works with the following Python interpreter implementations:
 
-- `CPython <https://www.python.org/>`_ versions 3.7, 3.8, 3.9, 3.10, 3.11, 3.12
-- `PyPy <https://pypy.org/>`_ 3.7, 3.8, 3.9
+- `CPython <https://www.python.org/>`_: ``3.12 >= python_version >= 3.7``
+- `PyPy <https://pypy.org/>`_: ``3.9 >= python_version >= 3.7``
 
 This means virtualenv works on the latest patch version of each of these minor versions. Previous patch versions are
 supported on a best effort approach.
@@ -90,9 +93,12 @@ supported on a best effort approach.
 CPython is shipped in multiple forms, and each OS repackages it, often applying some customization along the way.
 Therefore we cannot say universally that we support all platforms, but rather specify some we test against. In case
 of ones not specified here the support is unknown, though likely will work. If you find some cases please open a feature
-request on our issue tracker. Note, as of ``20.18.0`` we no longer support running under Python less than 3.7. Also, as
-of ``20.22.0`` we no longer support creating environments for Python 2.7, 3.5 and 3.6.
+request on our issue tracker.
 
+Note:
+
+- as of ``20.18.0`` -- ``2023-02-06`` -- we no longer support running under Python ``<=3.6``,
+- as of ``20.22.0`` -- ``2023-04-19`` -- we no longer support creating environments for Python ``<=3.6``.
 
 Linux
 ~~~~~
@@ -107,19 +113,11 @@ macOS
 ~~~~~
 In case of macOS we support:
 
-- installations from `python.org <https://www.python.org/downloads/>`_
-- python versions installed via `brew <https://docs.brew.sh/Homebrew-and-Python>`_ (both older python2.7 and python3)
-- Python 3 part of XCode (Python framework - ``/Library/Frameworks/Python3.framework/``)
-- Python 2 part of the OS (``/System/Library/Frameworks/Python.framework/Versions/``)
+- installations from `python.org <https://www.python.org/downloads/>`_,
+- python versions installed via `brew <https://docs.brew.sh/Homebrew-and-Python>`_,
+- Python 3 part of XCode (Python framework - ``/Library/Frameworks/Python3.framework/``).
 
 Windows
 ~~~~~~~
 - Installations from `python.org <https://www.python.org/downloads/>`_
 - Windows Store Python - note only `version 3.7+ <https://www.microsoft.com/en-us/p/python-38/9mssztt1n39l>`_
-
-Packaging variants
-~~~~~~~~~~~~~~~~~~
-- Normal variant (file structure as comes from `python.org <https://www.python.org/downloads/>`_).
-- We support CPython 2 system installations that do not contain the python files for the standard library if the
-  respective compiled files are present (e.g. only ``os.pyc``, not ``os.py``). This can be used by custom systems may
-  want to maximize available storage or obfuscate source code by removing ``.py`` files.
diff --git a/pyproject.toml b/pyproject.toml
index 8df35ef5..6efae4f9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -83,6 +83,7 @@ nushell = "virtualenv.activation.nushell:NushellActivator"
 powershell = "virtualenv.activation.powershell:PowerShellActivator"
 python = "virtualenv.activation.python:PythonActivator"
 [project.entry-points."virtualenv.create"]
+cpython3-mac-brew = "virtualenv.create.via_global_ref.builtin.cpython.mac_os:CPython3macOsBrew"
 cpython3-mac-framework = "virtualenv.create.via_global_ref.builtin.cpython.mac_os:CPython3macOsFramework"
 cpython3-posix = "virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Posix"
 cpython3-win = "virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Windows"
diff --git a/src/virtualenv/create/via_global_ref/api.py b/src/virtualenv/create/via_global_ref/api.py
index 0d362652..22a5a912 100644
--- a/src/virtualenv/create/via_global_ref/api.py
+++ b/src/virtualenv/create/via_global_ref/api.py
@@ -59,10 +59,10 @@ class ViaGlobalRefApi(Creator, metaclass=ABCMeta):
             dest="system_site",
             help="give the virtual environment access to the system site-packages dir",
         )
-        group = parser.add_mutually_exclusive_group()
         if not meta.can_symlink and not meta.can_copy:
             msg = "neither symlink or copy method supported"
             raise RuntimeError(msg)
+        group = parser.add_mutually_exclusive_group()
         if meta.can_symlink:
             group.add_argument(
                 "--symlinks",
diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/common.py b/src/virtualenv/create/via_global_ref/builtin/cpython/common.py
index 7c7abd5b..66f24468 100644
--- a/src/virtualenv/create/via_global_ref/builtin/cpython/common.py
+++ b/src/virtualenv/create/via_global_ref/builtin/cpython/common.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import re
 from abc import ABCMeta
 from collections import OrderedDict
 from pathlib import Path
@@ -54,9 +55,19 @@ def is_mac_os_framework(interpreter):
     return False
 
 
+def is_macos_brew(interpreter):
+    return interpreter.platform == "darwin" and _BREW.fullmatch(interpreter.system_prefix) is not None
+
+
+_BREW = re.compile(
+    r"/(usr/local|opt/homebrew)/(opt/python@3\.\d{1,2}|Cellar/python@3\.\d{1,2}/3\.\d{1,2}\.\d{1,2})/Frameworks/"
+    r"Python\.framework/Versions/3\.\d{1,2}",
+)
+
 __all__ = [
     "CPython",
     "CPythonPosix",
     "CPythonWindows",
     "is_mac_os_framework",
+    "is_macos_brew",
 ]
diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
index 0b7b023f..c1a1ce4a 100644
--- a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
+++ b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
@@ -11,7 +11,7 @@ from virtualenv.create.describe import Python3Supports
 from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest
 from virtualenv.create.via_global_ref.store import is_store_python
 
-from .common import CPython, CPythonPosix, CPythonWindows, is_mac_os_framework
+from .common import CPython, CPythonPosix, CPythonWindows, is_mac_os_framework, is_macos_brew
 
 
 class CPython3(CPython, Python3Supports, metaclass=abc.ABCMeta):
@@ -21,7 +21,11 @@ class CPython3(CPython, Python3Supports, metaclass=abc.ABCMeta):
 class CPython3Posix(CPythonPosix, CPython3):
     @classmethod
     def can_describe(cls, interpreter):
-        return is_mac_os_framework(interpreter) is False and super().can_describe(interpreter)
+        return (
+            is_mac_os_framework(interpreter) is False
+            and is_macos_brew(interpreter) is False
+            and super().can_describe(interpreter)
+        )
 
     def env_patch_text(self):
         text = super().env_patch_text()
diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py b/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py
index 42c191fc..3c4bda10 100644
--- a/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py
+++ b/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py
@@ -14,8 +14,9 @@ from virtualenv.create.via_global_ref.builtin.ref import (
     PathRefToDest,
     RefMust,
 )
+from virtualenv.create.via_global_ref.builtin.via_global_self_do import BuiltinViaGlobalRefMeta
 
-from .common import CPython, CPythonPosix, is_mac_os_framework
+from .common import CPython, CPythonPosix, is_mac_os_framework, is_macos_brew
 from .cpython3 import CPython3
 
 
@@ -258,7 +259,20 @@ def _builtin_change_mach_o(maxint):  # noqa: C901
     return mach_o_change
 
 
+class CPython3macOsBrew(CPython3, CPythonPosix):
+    @classmethod
+    def can_describe(cls, interpreter):
+        return is_macos_brew(interpreter) and super().can_describe(interpreter)
+
+    @classmethod
+    def setup_meta(cls, interpreter):  # noqa: ARG003
+        meta = BuiltinViaGlobalRefMeta()
+        meta.copy_error = "Brew disables copy creation: https://github.com/Homebrew/homebrew-core/issues/138159"
+        return meta
+
+
 __all__ = [
     "CPythonmacOsFramework",
     "CPython3macOsFramework",
+    "CPython3macOsBrew",
 ]
diff --git a/src/virtualenv/create/via_global_ref/venv.py b/src/virtualenv/create/via_global_ref/venv.py
index cc73a6f2..c283daf2 100644
--- a/src/virtualenv/create/via_global_ref/venv.py
+++ b/src/virtualenv/create/via_global_ref/venv.py
@@ -10,6 +10,7 @@ from virtualenv.util.path import ensure_dir
 from virtualenv.util.subprocess import run_cmd
 
 from .api import ViaGlobalRefApi, ViaGlobalRefMeta
+from .builtin.cpython.mac_os import CPython3macOsBrew
 from .builtin.pypy.pypy3 import Pypy3Windows
 
 
@@ -27,6 +28,8 @@ class Venv(ViaGlobalRefApi):
     @classmethod
     def can_create(cls, interpreter):
         if interpreter.has_venv:
+            if CPython3macOsBrew.can_describe(interpreter):
+                return CPython3macOsBrew.setup_meta(interpreter)
             meta = ViaGlobalRefMeta()
             if interpreter.platform == "win32":
                 meta = handle_store_python(meta, interpreter)


Test patch:
diff --git a/tests/unit/config/test_env_var.py b/tests/unit/config/test_env_var.py
index 751944bb..926e1d78 100644
--- a/tests/unit/config/test_env_var.py
+++ b/tests/unit/config/test_env_var.py
@@ -7,6 +7,8 @@ import pytest
 
 from virtualenv.config.cli.parser import VirtualEnvOptions
 from virtualenv.config.ini import IniConfig
+from virtualenv.create.via_global_ref.builtin.cpython.common import is_macos_brew
+from virtualenv.discovery.py_info import PythonInfo
 from virtualenv.run import session_via_cli
 
 
@@ -74,6 +76,7 @@ def test_extra_search_dir_via_env_var(tmp_path, monkeypatch):
 
 
 @pytest.mark.usefixtures("_empty_conf")
+@pytest.mark.skipif(is_macos_brew(PythonInfo.current_system()), reason="no copy on brew")
 def test_value_alias(monkeypatch, mocker):
     from virtualenv.config.cli.parser import VirtualEnvConfigParser
 
diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py
index b1f2921c..cfb38a60 100644
--- a/tests/unit/create/test_creator.py
+++ b/tests/unit/create/test_creator.py
@@ -24,6 +24,7 @@ import pytest
 from virtualenv.__main__ import run, run_with_catch
 from virtualenv.create.creator import DEBUG_SCRIPT, Creator, get_env_debug_info
 from virtualenv.create.pyenv_cfg import PyEnvCfg
+from virtualenv.create.via_global_ref.builtin.cpython.common import is_macos_brew
 from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Posix
 from virtualenv.discovery.py_info import PythonInfo
 from virtualenv.info import IS_PYPY, IS_WIN, fs_is_case_sensitive
@@ -430,6 +431,7 @@ def list_files(path):
     return result
 
 
+@pytest.mark.skipif(is_macos_brew(CURRENT), reason="no copy on brew")
 def test_zip_importer_can_import_setuptools(tmp_path):
     """We're patching the loaders so might fail on r/o loaders, such as zipimporter on CPython<3.8"""
     result = cli_run(