diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa37add60..bb581af38 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.2 + rev: 0.28.3 hooks: - id: check-github-workflows args: [ "--verbose" ] @@ -20,12 +20,11 @@ repos: - id: tox-ini-fmt args: ["-p", "fix"] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "1.8.0" + rev: "2.0.4" hooks: - id: pyproject-fmt - additional_dependencies: ["tox>=4.12.1"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.4.1" + rev: "v0.4.4" hooks: - id: ruff-format - id: ruff diff --git a/docs/changelog.rst b/docs/changelog.rst index e3749431c..1b0c0047a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,14 @@ Release History .. towncrier release notes start +v20.26.2 (2024-05-13) +--------------------- + +Bugfixes - 20.26.2 +~~~~~~~~~~~~~~~~~~ +- ``virtualenv.pyz`` no longer fails when zipapp path contains a symlink - by :user:`HandSonic` and :user:`petamas`. (:issue:`1949`) +- Fix bad return code from activate.sh if hashing is disabled - by :user:'fenkes-ibm'. (:issue:`2717`) + v20.26.1 (2024-04-29) --------------------- diff --git a/pyproject.toml b/pyproject.toml index 0aaa3db7e..49cd2388b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,9 @@ keywords = [ "virtual", ] license = "MIT" -maintainers = [{ name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }] +maintainers = [ + { name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }, +] requires-python = ">=3.7" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -31,7 +33,6 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries", @@ -44,7 +45,7 @@ dynamic = [ dependencies = [ "distlib<1,>=0.3.7", "filelock<4,>=3.12.2", - 'importlib-metadata>=6.6; python_version < "3.8"', + "importlib-metadata>=6.6; python_version<'3.8'", "platformdirs<5,>=3.9.1", ] optional-dependencies.docs = [ @@ -63,50 +64,57 @@ optional-dependencies.test = [ "packaging>=23.1", "pytest>=7.4", "pytest-env>=0.8.2", - 'pytest-freezer>=0.4.8; platform_python_implementation == "PyPy"', + "pytest-freezer>=0.4.8; platform_python_implementation=='PyPy'", "pytest-mock>=3.11.1", "pytest-randomly>=3.12", "pytest-timeout>=2.1", "setuptools>=68", - 'time-machine>=2.10; platform_python_implementation == "CPython"', + "time-machine>=2.10; platform_python_implementation=='CPython'", ] urls.Documentation = "https://virtualenv.pypa.io" urls.Homepage = "https://github.com/pypa/virtualenv" urls.Source = "https://github.com/pypa/virtualenv" urls.Tracker = "https://github.com/pypa/virtualenv/issues" scripts.virtualenv = "virtualenv.__main__:run_with_catch" -[project.entry-points."virtualenv.activate"] -bash = "virtualenv.activation.bash:BashActivator" -batch = "virtualenv.activation.batch:BatchActivator" -cshell = "virtualenv.activation.cshell:CShellActivator" -fish = "virtualenv.activation.fish:FishActivator" -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" -pypy3-posix = "virtualenv.create.via_global_ref.builtin.pypy.pypy3:PyPy3Posix" -pypy3-win = "virtualenv.create.via_global_ref.builtin.pypy.pypy3:Pypy3Windows" -venv = "virtualenv.create.via_global_ref.venv:Venv" -[project.entry-points."virtualenv.discovery"] -builtin = "virtualenv.discovery.builtin:Builtin" -[project.entry-points."virtualenv.seed"] -app-data = "virtualenv.seed.embed.via_app_data.via_app_data:FromAppData" -pip = "virtualenv.seed.embed.pip_invoke:PipInvoke" +entry-points."virtualenv.activate".bash = "virtualenv.activation.bash:BashActivator" +entry-points."virtualenv.activate".batch = "virtualenv.activation.batch:BatchActivator" +entry-points."virtualenv.activate".cshell = "virtualenv.activation.cshell:CShellActivator" +entry-points."virtualenv.activate".fish = "virtualenv.activation.fish:FishActivator" +entry-points."virtualenv.activate".nushell = "virtualenv.activation.nushell:NushellActivator" +entry-points."virtualenv.activate".powershell = "virtualenv.activation.powershell:PowerShellActivator" +entry-points."virtualenv.activate".python = "virtualenv.activation.python:PythonActivator" +entry-points."virtualenv.create".cpython3-mac-brew = "virtualenv.create.via_global_ref.builtin.cpython.mac_os:CPython3macOsBrew" +entry-points."virtualenv.create".cpython3-mac-framework = "virtualenv.create.via_global_ref.builtin.cpython.mac_os:CPython3macOsFramework" +entry-points."virtualenv.create".cpython3-posix = "virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Posix" +entry-points."virtualenv.create".cpython3-win = "virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Windows" +entry-points."virtualenv.create".pypy3-posix = "virtualenv.create.via_global_ref.builtin.pypy.pypy3:PyPy3Posix" +entry-points."virtualenv.create".pypy3-win = "virtualenv.create.via_global_ref.builtin.pypy.pypy3:Pypy3Windows" +entry-points."virtualenv.create".venv = "virtualenv.create.via_global_ref.venv:Venv" +entry-points."virtualenv.discovery".builtin = "virtualenv.discovery.builtin:Builtin" +entry-points."virtualenv.seed".app-data = "virtualenv.seed.embed.via_app_data.via_app_data:FromAppData" +entry-points."virtualenv.seed".pip = "virtualenv.seed.embed.pip_invoke:PipInvoke" [tool.hatch] build.hooks.vcs.version-file = "src/virtualenv/version.py" -build.targets.sdist.include = ["/src", "/tests", "/tasks", "/tox.ini"] +build.targets.sdist.include = [ + "/src", + "/tests", + "/tasks", + "/tox.ini", +] version.source = "vcs" [tool.ruff] line-length = 120 target-version = "py37" -lint.isort = { known-first-party = ["virtualenv"], required-imports = ["from __future__ import annotations"] } -lint.select = ["ALL"] +lint.isort = { known-first-party = [ + "virtualenv", +], required-imports = [ + "from __future__ import annotations", +] } +lint.select = [ + "ALL", +] lint.ignore = [ "CPY", # No copyright header "ANN", # no type checking added yet @@ -144,10 +152,14 @@ builtin = "clear,usage,en-GB_to_en-US" count = true [tool.pytest.ini_options] -markers = ["slow"] +markers = [ + "slow", +] timeout = 600 addopts = "--showlocals --no-success-flaky-report" -env = ["PYTHONIOENCODING=utf-8"] +env = [ + "PYTHONIOENCODING=utf-8", +] [tool.coverage] html.show_contexts = true @@ -159,12 +171,20 @@ report.omit = [ "**/src/virtualenv/activation/python/activate_this.py", "**/src/virtualenv/seed/wheels/embed/pip-*.whl/pip/**", ] -paths.source = ["src", "**/site-packages"] +paths.source = [ + "src", + "**/site-packages", +] report.fail_under = 76 -run.source = ["${_COVERAGE_SRC}", "tests"] +run.source = [ + "${_COVERAGE_SRC}", + "tests", +] run.dynamic_context = "test_function" run.parallel = true -run.plugins = ["covdefaults"] +run.plugins = [ + "covdefaults", +] run.relative_files = true [tool.towncrier] diff --git a/src/virtualenv/__main__.py b/src/virtualenv/__main__.py index 2dc97c787..d0979a665 100644 --- a/src/virtualenv/__main__.py +++ b/src/virtualenv/__main__.py @@ -21,7 +21,7 @@ def run(args=None, options=None, env=None): print(f"subprocess call failed for {exception.cmd} with code {exception.code}") # noqa: T201 print(exception.out, file=sys.stdout, end="") # noqa: T201 print(exception.err, file=sys.stderr, end="") # noqa: T201 - raise SystemExit(exception.code) # noqa: TRY200, B904 + raise SystemExit(exception.code) # noqa: B904 class LogSession: diff --git a/src/virtualenv/activation/bash/activate.sh b/src/virtualenv/activation/bash/activate.sh index b06e3fd33..04d5f5e05 100644 --- a/src/virtualenv/activation/bash/activate.sh +++ b/src/virtualenv/activation/bash/activate.sh @@ -84,4 +84,4 @@ pydoc () { # The hash command must be called to get it to forget past # commands. Without forgetting past commands the $PATH changes # we made may not be respected -hash -r 2>/dev/null +hash -r 2>/dev/null || true diff --git a/src/virtualenv/activation/python/activate_this.py b/src/virtualenv/activation/python/activate_this.py index befe8f405..388e00153 100644 --- a/src/virtualenv/activation/python/activate_this.py +++ b/src/virtualenv/activation/python/activate_this.py @@ -1,7 +1,8 @@ """ Activate virtualenv for current interpreter: -Use exec(open(this_file).read(), {'__file__': this_file}). +import runpy +runpy.run_path(this_file) This can be used when you must use an existing Python interpreter, not the virtualenv bin/python. """ # noqa: D415 @@ -15,7 +16,7 @@ try: abs_file = os.path.abspath(__file__) except NameError as exc: - msg = "You must use exec(open(this_file).read(), {'__file__': this_file})" + msg = "You must use import runpy; runpy.run_path(this_file)" raise AssertionError(msg) from exc bin_dir = os.path.dirname(abs_file) diff --git a/src/virtualenv/create/debug.py b/src/virtualenv/create/debug.py index 3f54685a3..bc33367c8 100644 --- a/src/virtualenv/create/debug.py +++ b/src/virtualenv/create/debug.py @@ -95,7 +95,7 @@ def run(): # noqa: PLR0912 except (ValueError, TypeError) as exception: # pragma: no cover sys.stderr.write(repr(exception)) sys.stdout.write(repr(result)) # pragma: no cover - raise SystemExit(1) # noqa: TRY200, B904 # pragma: no cover + raise SystemExit(1) # noqa: B904 # pragma: no cover if __name__ == "__main__": diff --git a/src/virtualenv/create/describe.py b/src/virtualenv/create/describe.py index 726305547..1ee250cbc 100644 --- a/src/virtualenv/create/describe.py +++ b/src/virtualenv/create/describe.py @@ -7,7 +7,7 @@ from virtualenv.info import IS_WIN -class Describe(ABC): +class Describe: """Given a host interpreter tell us information about what the created interpreter might look like.""" suffix = ".exe" if IS_WIN else "" diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index 7fe1f54c1..882daa331 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -383,7 +383,7 @@ def from_exe( # noqa: PLR0913 if isinstance(proposed, PythonInfo) and resolve_to_host: try: proposed = proposed._resolve_to_system(app_data, proposed) # noqa: SLF001 - except Exception as exception: # noqa: BLE001 + except Exception as exception: if raise_on_error: raise logging.info("ignore %s due cannot resolve system due to %r", proposed.original_executable, exception) diff --git a/src/virtualenv/util/zipapp.py b/src/virtualenv/util/zipapp.py index f736e3763..958db1543 100644 --- a/src/virtualenv/util/zipapp.py +++ b/src/virtualenv/util/zipapp.py @@ -23,8 +23,12 @@ def extract(full_path, dest): def _get_path_within_zip(full_path): - full_path = os.path.abspath(str(full_path)) - sub_file = full_path[len(ROOT) + 1 :] + full_path = os.path.realpath(os.path.abspath(str(full_path))) + prefix = f"{ROOT}{os.sep}" + if not full_path.startswith(prefix): + msg = f"full_path={full_path} should start with prefix={prefix}." + raise RuntimeError(msg) + sub_file = full_path[len(prefix) :] if IS_WIN: # paths are always UNIX separators, even on Windows, though __file__ still follows platform default sub_file = sub_file.replace(os.sep, "/") diff --git a/tasks/make_zipapp.py b/tasks/make_zipapp.py index f83b9fff3..dec0f1fd1 100644 --- a/tasks/make_zipapp.py +++ b/tasks/make_zipapp.py @@ -52,7 +52,7 @@ def create_zipapp(dest, packages): print(f"zipapp created at {dest}") # noqa: T201 -def write_packages_to_zipapp(base, dist, modules, packages, zip_app): # noqa: C901 +def write_packages_to_zipapp(base, dist, modules, packages, zip_app): # noqa: C901, PLR0912 has = set() for name, p_w_v in packages.items(): # noqa: PLR1702 for platform, w_v in p_w_v.items(): @@ -180,7 +180,7 @@ def get_dependencies(whl, version): platforms = [] platform_positions = WheelDownloader._marker_at(markers, "sys_platform") deleted = 0 - for pos in platform_positions: # can only be ore meaningfully + for pos in platform_positions: # can only be or meaningfully platform = f"{markers[pos][1].value}{markers[pos][2].value}" deleted += WheelDownloader._del_marker_at(markers, pos - deleted) platforms.append(platform) diff --git a/tests/integration/test_zipapp.py b/tests/integration/test_zipapp.py index 7157a9e75..dfc6d9759 100644 --- a/tests/integration/test_zipapp.py +++ b/tests/integration/test_zipapp.py @@ -9,6 +9,7 @@ from flaky import flaky from virtualenv.discovery.py_info import PythonInfo +from virtualenv.info import fs_supports_symlink from virtualenv.run import cli_run HERE = Path(__file__).parent @@ -83,6 +84,24 @@ def _run(*args): return _run +@pytest.fixture() +def call_zipapp_symlink(zipapp, tmp_path, zipapp_test_env, temp_app_data): # noqa: ARG001 + def _run(*args): + symlinked = zipapp.parent / "symlinked_virtualenv.pyz" + symlinked.symlink_to(str(zipapp)) + cmd = [str(zipapp_test_env), str(symlinked), "-vv", str(tmp_path / "env"), *list(args)] + subprocess.check_call(cmd) + + return _run + + +@pytest.mark.skipif(not fs_supports_symlink(), reason="symlink not supported") +def test_zipapp_in_symlink(capsys, call_zipapp_symlink): + call_zipapp_symlink("--reset-app-data") + _out, err = capsys.readouterr() + assert not err + + @flaky(max_runs=2, min_passes=1) def test_zipapp_help(call_zipapp, capsys): call_zipapp("-h") diff --git a/tests/unit/activation/conftest.py b/tests/unit/activation/conftest.py index 623c55d4e..87e3307aa 100644 --- a/tests/unit/activation/conftest.py +++ b/tests/unit/activation/conftest.py @@ -41,7 +41,7 @@ def get_version(self, raise_on_fail): encoding="utf-8", ) out, err = process.communicate() - except Exception as exception: # noqa: BLE001 + except Exception as exception: self._version = exception if raise_on_fail: raise @@ -79,6 +79,7 @@ def __call__(self, monkeypatch, tmp_path): process = Popen(invoke, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) raw_, _ = process.communicate() raw = raw_.decode() + assert process.returncode == 0, raw except subprocess.CalledProcessError as exception: output = exception.output + exception.stderr assert not exception.returncode, output # noqa: PT017 diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py index 4c92b733c..d89f1606a 100644 --- a/tests/unit/activation/test_bash.py +++ b/tests/unit/activation/test_bash.py @@ -7,7 +7,8 @@ @pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") -def test_bash(raise_on_non_source_class, activation_tester): +@pytest.mark.parametrize("hashing_enabled", [True, False]) +def test_bash(raise_on_non_source_class, hashing_enabled, activation_tester): class Bash(raise_on_non_source_class): def __init__(self, session) -> None: super().__init__( @@ -18,6 +19,11 @@ def __init__(self, session) -> None: "sh", "You must source this script: $ source ", ) + self.deactivate += " || exit 1" + self._invoke_script.append("-h" if hashing_enabled else "+h") + + def activate_call(self, script): + return super().activate_call(script) + " || exit 1" def print_prompt(self): return self.print_os_env_var("PS1") diff --git a/tests/unit/activation/test_python_activator.py b/tests/unit/activation/test_python_activator.py index 3da4331b1..24a3561c5 100644 --- a/tests/unit/activation/test_python_activator.py +++ b/tests/unit/activation/test_python_activator.py @@ -18,7 +18,7 @@ def __init__(self, session) -> None: sys.executable, activate_script="activate_this.py", extension="py", - non_source_fail_message="You must use exec(open(this_file).read(), {'__file__': this_file})", + non_source_fail_message="You must use import runpy; runpy.run_path(this_file)", ) self.unix_line_ending = not IS_WIN @@ -36,6 +36,7 @@ def _get_test_lines(activate_script): import os import sys import platform + import runpy def print_r(value): print(repr(value)) @@ -47,10 +48,7 @@ def print_r(value): file_at = {str(activate_script)!r} # CPython 2 requires non-ascii path open to be unicode - with open(file_at, "r", encoding='utf-8') as file_handler: - content = file_handler.read() - exec(content, {{"__file__": file_at}}) - + runpy.run_path(file_at) print_r(os.environ.get("VIRTUAL_ENV")) print_r(os.environ.get("VIRTUAL_ENV_PROMPT")) print_r(os.environ.get("PATH").split(os.pathsep))