diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml
index 512e54ba6..a5b66730a 100644
--- a/.github/workflows/check.yaml
+++ b/.github/workflows/check.yaml
@@ -39,9 +39,6 @@ jobs:
- { 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" }
- - { os: ubuntu-latest, py: "3.7" }
- - { os: macos-13, py: "3.7" }
exclude:
- { os: windows-latest, py: "pypy-3.10" }
- { os: windows-latest, py: "pypy-3.9" }
diff --git a/docs/changelog/2758.feature.rst b/docs/changelog/2758.feature.rst
new file mode 100644
index 000000000..33ffce17e
--- /dev/null
+++ b/docs/changelog/2758.feature.rst
@@ -0,0 +1 @@
+Drop 3.7 support as the CI environments no longer allow it running - by :user:`gaborbernat`.
diff --git a/docs/changelog/2783.bugfix.rst b/docs/changelog/2783.bugfix.rst
new file mode 100644
index 000000000..8de3034e2
--- /dev/null
+++ b/docs/changelog/2783.bugfix.rst
@@ -0,0 +1,8 @@
+Upgrade embedded wheels:
+
+* setuptools to ``75.2.0`` from ``75.1.0``
+* Removed pip of ``24.0``
+* Removed setuptools of ``68.0.0``
+* Removed wheel of ``0.42.0``
+
+- by :user:`gaborbernat`.
diff --git a/docs/changelog/2784.bugfix.rst b/docs/changelog/2784.bugfix.rst
new file mode 100644
index 000000000..88b13c252
--- /dev/null
+++ b/docs/changelog/2784.bugfix.rst
@@ -0,0 +1 @@
+Fix zipapp is broken on Windows post distlib ``0.3.9`` - by :user:`gaborbernat`.
diff --git a/docs/installation.rst b/docs/installation.rst
index a25c3fef6..c48db8f07 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -97,6 +97,7 @@ request on our issue tracker.
Note:
+- as of ``20.27.0`` -- ``2024-10-17`` -- we no longer support running under Python ``<=3.7``,
- 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``.
@@ -120,4 +121,4 @@ In case of macOS we support:
Windows
~~~~~~~
- Installations from `python.org `_
-- Windows Store Python - note only `version 3.7+ `_
+- Windows Store Python - note only `version 3.8+ `_
diff --git a/pyproject.toml b/pyproject.toml
index fabf43460..2bd8c62ab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,7 +18,7 @@ license = "MIT"
maintainers = [
{ name = "Bernat Gabor", email = "gaborjbernat@gmail.com" },
]
-requires-python = ">=3.7"
+requires-python = ">=3.8"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
@@ -27,7 +27,6 @@ classifiers = [
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
@@ -106,7 +105,6 @@ build.targets.sdist.include = [
version.source = "vcs"
[tool.ruff]
-target-version = "py37"
line-length = 120
format.preview = true
format.docstring-code-line-length = 100
@@ -128,6 +126,7 @@ lint.ignore = [
"PLR0914", # Too many local variables
"PLR0917", # Too many positional arguments
"PLR6301", # Method could be a function, class method, or static method
+ "PLW1510", # no need for check for subprocess
"PTH", # no pathlib, <=39 has problems on Windows with absolute/resolve, can revisit once we no longer need 39
"S104", # Possible binding to all interfaces
"S404", # Using subprocess is alright
diff --git a/src/virtualenv/run/plugin/base.py b/src/virtualenv/run/plugin/base.py
index f0682ddf0..97c4792ec 100644
--- a/src/virtualenv/run/plugin/base.py
+++ b/src/virtualenv/run/plugin/base.py
@@ -2,15 +2,9 @@
import sys
from collections import OrderedDict
+from importlib.metadata import entry_points
-if sys.version_info >= (3, 8):
- from importlib.metadata import entry_points
-
- importlib_metadata_version = ()
-else:
- from importlib_metadata import entry_points, version
-
- importlib_metadata_version = tuple(int(i) for i in version("importlib_metadata").split(".")[:2])
+importlib_metadata_version = ()
class PluginLoader:
diff --git a/src/virtualenv/seed/wheels/embed/__init__.py b/src/virtualenv/seed/wheels/embed/__init__.py
index f068efc7c..ce8690e91 100644
--- a/src/virtualenv/seed/wheels/embed/__init__.py
+++ b/src/virtualenv/seed/wheels/embed/__init__.py
@@ -6,48 +6,43 @@
BUNDLE_FOLDER = Path(__file__).absolute().parent
BUNDLE_SUPPORT = {
- "3.7": {
- "pip": "pip-24.0-py3-none-any.whl",
- "setuptools": "setuptools-68.0.0-py3-none-any.whl",
- "wheel": "wheel-0.42.0-py3-none-any.whl",
- },
"3.8": {
"pip": "pip-24.2-py3-none-any.whl",
- "setuptools": "setuptools-75.1.0-py3-none-any.whl",
+ "setuptools": "setuptools-75.2.0-py3-none-any.whl",
"wheel": "wheel-0.44.0-py3-none-any.whl",
},
"3.9": {
"pip": "pip-24.2-py3-none-any.whl",
- "setuptools": "setuptools-75.1.0-py3-none-any.whl",
+ "setuptools": "setuptools-75.2.0-py3-none-any.whl",
"wheel": "wheel-0.44.0-py3-none-any.whl",
},
"3.10": {
"pip": "pip-24.2-py3-none-any.whl",
- "setuptools": "setuptools-75.1.0-py3-none-any.whl",
+ "setuptools": "setuptools-75.2.0-py3-none-any.whl",
"wheel": "wheel-0.44.0-py3-none-any.whl",
},
"3.11": {
"pip": "pip-24.2-py3-none-any.whl",
- "setuptools": "setuptools-75.1.0-py3-none-any.whl",
+ "setuptools": "setuptools-75.2.0-py3-none-any.whl",
"wheel": "wheel-0.44.0-py3-none-any.whl",
},
"3.12": {
"pip": "pip-24.2-py3-none-any.whl",
- "setuptools": "setuptools-75.1.0-py3-none-any.whl",
+ "setuptools": "setuptools-75.2.0-py3-none-any.whl",
"wheel": "wheel-0.44.0-py3-none-any.whl",
},
"3.13": {
"pip": "pip-24.2-py3-none-any.whl",
- "setuptools": "setuptools-75.1.0-py3-none-any.whl",
+ "setuptools": "setuptools-75.2.0-py3-none-any.whl",
"wheel": "wheel-0.44.0-py3-none-any.whl",
},
"3.14": {
"pip": "pip-24.2-py3-none-any.whl",
- "setuptools": "setuptools-75.1.0-py3-none-any.whl",
+ "setuptools": "setuptools-75.2.0-py3-none-any.whl",
"wheel": "wheel-0.44.0-py3-none-any.whl",
},
}
-MAX = "3.7"
+MAX = "3.8"
def get_embed_wheel(distribution, for_py_version):
diff --git a/src/virtualenv/seed/wheels/embed/pip-24.0-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/pip-24.0-py3-none-any.whl
deleted file mode 100644
index 2e6aa9d2c..000000000
Binary files a/src/virtualenv/seed/wheels/embed/pip-24.0-py3-none-any.whl and /dev/null differ
diff --git a/src/virtualenv/seed/wheels/embed/setuptools-68.0.0-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/setuptools-68.0.0-py3-none-any.whl
deleted file mode 100644
index 81f15459b..000000000
Binary files a/src/virtualenv/seed/wheels/embed/setuptools-68.0.0-py3-none-any.whl and /dev/null differ
diff --git a/src/virtualenv/seed/wheels/embed/setuptools-75.1.0-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/setuptools-75.2.0-py3-none-any.whl
similarity index 88%
rename from src/virtualenv/seed/wheels/embed/setuptools-75.1.0-py3-none-any.whl
rename to src/virtualenv/seed/wheels/embed/setuptools-75.2.0-py3-none-any.whl
index b7f887bd4..f476c4053 100644
Binary files a/src/virtualenv/seed/wheels/embed/setuptools-75.1.0-py3-none-any.whl and b/src/virtualenv/seed/wheels/embed/setuptools-75.2.0-py3-none-any.whl differ
diff --git a/src/virtualenv/seed/wheels/embed/wheel-0.42.0-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/wheel-0.42.0-py3-none-any.whl
deleted file mode 100644
index 8044b1982..000000000
Binary files a/src/virtualenv/seed/wheels/embed/wheel-0.42.0-py3-none-any.whl and /dev/null differ
diff --git a/tasks/__main__zipapp.py b/tasks/__main__zipapp.py
index 01e3efaf2..9118e2007 100644
--- a/tasks/__main__zipapp.py
+++ b/tasks/__main__zipapp.py
@@ -4,22 +4,25 @@
import os
import sys
import zipfile
+from functools import cached_property
+from importlib.abc import SourceLoader
+from importlib.util import spec_from_file_location
ABS_HERE = os.path.abspath(os.path.dirname(__file__))
-NEW_IMPORT_SYSTEM = sys.version_info[0] >= 3 # noqa: PLR2004
class VersionPlatformSelect:
def __init__(self) -> None:
- self.archive = ABS_HERE
- self._zip_file = zipfile.ZipFile(ABS_HERE, "r")
+ zipapp = ABS_HERE
+ self.archive = zipapp
+ self._zip_file = zipfile.ZipFile(zipapp)
self.modules = self._load("modules.json")
self.distributions = self._load("distributions.json")
self.__cache = {}
def _load(self, of_file):
version = ".".join(str(i) for i in sys.version_info[0:2])
- per_version = json.loads(self.get_data(of_file).decode("utf-8"))
+ per_version = json.loads(self.get_data(of_file).decode())
all_platforms = per_version[version] if version in per_version else per_version["3.9"]
content = all_platforms.get("==any", {}) # start will all platforms
not_us = f"!={sys.platform}"
@@ -65,25 +68,66 @@ def find_distributions(self, context):
def __repr__(self) -> str:
return f"{self.__class__.__name__}(path={ABS_HERE})"
- def _register_distutils_finder(self):
+ def _register_distutils_finder(self): # noqa: C901
if "distlib" not in self.modules:
return
+ class Resource:
+ def __init__(self, path: str, name: str, loader: SourceLoader) -> None:
+ self.path = os.path.join(path, name)
+ self._name = name
+ self.loader = loader
+
+ @cached_property
+ def name(self) -> str:
+ return os.path.basename(self._name)
+
+ @property
+ def bytes(self) -> bytes:
+ return self.loader.get_data(self._name)
+
+ @property
+ def is_container(self) -> bool:
+ return len(self.resources) > 1
+
+ @cached_property
+ def resources(self) -> list[str]:
+ return [
+ i.filename
+ for i in (
+ (j for j in zip_file.filelist if j.filename.startswith(f"{self._name}/"))
+ if self._name
+ else zip_file.filelist
+ )
+ ]
+
class DistlibFinder:
def __init__(self, path, loader) -> None:
self.path = path
self.loader = loader
def find(self, name):
- class Resource:
- def __init__(self, content) -> None:
- self.bytes = content
-
- full_path = os.path.join(self.path, name)
- return Resource(self.loader.get_data(full_path))
+ return Resource(self.path, name, self.loader)
+
+ def iterator(self, resource_name):
+ resource = self.find(resource_name)
+ if resource is not None:
+ todo = [resource]
+ while todo:
+ resource = todo.pop(0)
+ yield resource
+ if resource.is_container:
+ resource_name = resource.name
+ for name in resource.resources:
+ child = self.find(f"{resource_name}/{name}" if resource_name else name)
+ if child.is_container:
+ todo.append(child)
+ else:
+ yield child
from distlib.resources import register_finder # noqa: PLC0415
+ zip_file = self._zip_file
register_finder(self, lambda module: DistlibFinder(os.path.dirname(module.__file__), self))
@@ -93,10 +137,7 @@ def __init__(self, content) -> None:
def versioned_distribution_class():
global _VER_DISTRIBUTION_CLASS # noqa: PLW0603
if _VER_DISTRIBUTION_CLASS is None:
- if sys.version_info >= (3, 8):
- from importlib.metadata import Distribution # noqa: PLC0415
- else:
- from importlib_metadata import Distribution # noqa: PLC0415
+ from importlib.metadata import Distribution # noqa: PLC0415
class VersionedDistribution(Distribution):
def __init__(self, file_loader, dist_path) -> None:
@@ -113,41 +154,15 @@ def locate_file(self, path):
return _VER_DISTRIBUTION_CLASS
-if NEW_IMPORT_SYSTEM:
- from importlib.abc import SourceLoader
- from importlib.util import spec_from_file_location
-
- class VersionedFindLoad(VersionPlatformSelect, SourceLoader):
- def find_spec(self, fullname, path, target=None): # noqa: ARG002
- zip_path = self.find_mod(fullname)
- if zip_path is not None:
- return spec_from_file_location(name=fullname, loader=self)
- return None
-
- def module_repr(self, module):
- raise NotImplementedError
-
-else:
- from imp import new_module
-
- class VersionedFindLoad(VersionPlatformSelect):
- def find_module(self, fullname, path=None): # noqa: ARG002
- return self if self.find_mod(fullname) else None
-
- def load_module(self, fullname):
- filename = self.get_filename(fullname)
- code = self.get_data(filename)
- mod = sys.modules.setdefault(fullname, new_module(fullname))
- mod.__file__ = filename
- mod.__loader__ = self
- is_package = filename.endswith("__init__.py")
- if is_package:
- mod.__path__ = [os.path.dirname(filename)]
- mod.__package__ = fullname
- else:
- mod.__package__ = fullname.rpartition(".")[0]
- exec(code, mod.__dict__) # noqa: S102
- return mod
+class VersionedFindLoad(VersionPlatformSelect, SourceLoader):
+ def find_spec(self, fullname, path, target=None): # noqa: ARG002
+ zip_path = self.find_mod(fullname)
+ if zip_path is not None:
+ return spec_from_file_location(name=fullname, loader=self)
+ return None
+
+ def module_repr(self, module):
+ raise NotImplementedError
def run():
diff --git a/tasks/make_zipapp.py b/tasks/make_zipapp.py
index aaf3ac34d..4fc07dac0 100644
--- a/tasks/make_zipapp.py
+++ b/tasks/make_zipapp.py
@@ -23,7 +23,7 @@
HERE = Path(__file__).parent.absolute()
-VERSIONS = [f"3.{i}" for i in range(10, 6, -1)]
+VERSIONS = [f"3.{i}" for i in range(13, 7, -1)]
def main():
@@ -49,7 +49,7 @@ def create_zipapp(dest, packages):
zip_app.writestr("__main__.py", (HERE / "__main__zipapp.py").read_bytes())
bio.seek(0)
zipapp.create_archive(bio, dest)
- print(f"zipapp created at {dest}") # noqa: T201
+ print(f"zipapp created at {dest} with size {os.path.getsize(dest) / 1024 / 1024:.2f}MB") # noqa: T201
def write_packages_to_zipapp(base, dist, modules, packages, zip_app): # noqa: C901, PLR0912
diff --git a/tasks/upgrade_wheels.py b/tasks/upgrade_wheels.py
index 031508632..8b4a1b508 100644
--- a/tasks/upgrade_wheels.py
+++ b/tasks/upgrade_wheels.py
@@ -15,7 +15,7 @@
STRICT = "UPGRADE_ADVISORY" not in os.environ
BUNDLED = ["pip", "setuptools", "wheel"]
-SUPPORT = [(3, i) for i in range(7, 15)]
+SUPPORT = [(3, i) for i in range(8, 15)]
DEST = Path(__file__).resolve().parents[1] / "src" / "virtualenv" / "seed" / "wheels" / "embed"
@@ -66,8 +66,8 @@ def run(): # noqa: C901
added = collect_package_versions(new_packages)
removed = collect_package_versions(remove_packages)
-
outcome = (1 if STRICT else 0) if (added or removed) else 0
+ print(f"Outcome {outcome} added {added} removed {removed}") # noqa: T201
lines = ["Upgrade embedded wheels:", ""]
for key, versions in added.items():
text = f"* {key} to {fmt_version(versions)}"
@@ -119,15 +119,8 @@ def get_embed_wheel(distribution, for_py_version):
)
dest_target = DEST / "__init__.py"
dest_target.write_text(msg, encoding="utf-8")
-
- subprocess.run(
- [sys.executable, "-m", "ruff", "check", str(dest_target), "--fix", "--unsafe-fixes"],
- check=False,
- )
- subprocess.run(
- [sys.executable, "-m", "ruff", "format", str(dest_target), "--preview"],
- check=False,
- )
+ subprocess.run([sys.executable, "-m", "ruff", "format", str(dest_target), "--preview"])
+ subprocess.run([sys.executable, "-m", "ruff", "check", str(dest_target), "--fix", "--unsafe-fixes"])
raise SystemExit(outcome)
diff --git a/tests/unit/seed/embed/test_pip_invoke.py b/tests/unit/seed/embed/test_pip_invoke.py
index 7b38d7cc9..d8c243e57 100644
--- a/tests/unit/seed/embed/test_pip_invoke.py
+++ b/tests/unit/seed/embed/test_pip_invoke.py
@@ -25,7 +25,7 @@ def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_f
def _load_embed_wheel(app_data, distribution, for_py_version, version): # noqa: ARG001
return load_embed_wheel(app_data, distribution, old_ver, version)
- old_ver = "3.7"
+ old_ver = "3.8"
old = BUNDLE_SUPPORT[old_ver]
mocker.patch("virtualenv.seed.wheels.bundle.load_embed_wheel", side_effect=_load_embed_wheel)
diff --git a/tox.ini b/tox.ini
index 50b2ab150..591ecd9f1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,7 +10,6 @@ env_list =
3.10
3.9
3.8
- 3.7
coverage
readme
docs