diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 81916ad43..866f08735 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: py: - - "3.12.0-alpha.7" + - "3.12.0-beta.2" - "3.11" - "3.10" - "3.9" @@ -29,12 +29,12 @@ jobs: - pypy-3.8 - pypy-3.7 os: - - ubuntu-22.04 - - macos-12 - - windows-2022 + - ubuntu-latest + - macos-latest + - windows-latest include: - - { os: macos-12, py: "brew@3.9" } - - { os: macos-12, py: "brew@3.8" } + - { os: macos-latest, py: "brew@3.9" } + - { os: macos-latest, py: "brew@3.8" } steps: - uses: taiki-e/install-action@cargo-binstall - name: Install OS dependencies @@ -100,8 +100,8 @@ jobs: fail-fast: false matrix: os: - - ubuntu-22.04 - - windows-2022 + - ubuntu-latest + - windows-latest tox_env: - dev - docs @@ -109,8 +109,8 @@ jobs: - upgrade - zipapp exclude: - - { os: windows-2022, tox_env: readme } - - { os: windows-2022, tox_env: docs } + - { os: windows-latest, tox_env: readme } + - { os: windows-latest, tox_env: docs } steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8303d863c..2dea97659 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: jobs: release: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest environment: name: release url: https://pypi.org/p/virtualenv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80bb9059c..f172caa58 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,78 +2,33 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - - id: check-ast - - id: check-builtin-literals - - id: check-docstring-first - - id: check-merge-conflict - - id: check-yaml - - id: check-toml - - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/asottile/add-trailing-comma - rev: v2.4.0 - hooks: - - id: add-trailing-comma - args: [--py36-plus] - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: ["--py37-plus"] - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - args: [--safe] - - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 - hooks: - - id: blacken-docs - additional_dependencies: [black==23.3] - - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.10.0 - hooks: - - id: rst-backticks - repo: https://github.com/tox-dev/tox-ini-fmt rev: "1.3.0" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + - repo: https://github.com/tox-dev/pyproject-fmt + rev: "0.11.2" hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear==23.3.23 - - flake8-comprehensions==3.12 - - flake8-pytest-style==1.7.2 - - flake8-spellcheck==0.28 - - flake8-unused-arguments==0.0.13 - - flake8-noqa==1.3.1 - - pep8-naming==0.13.3 - - flake8-pyproject==1.2.3 + - id: pyproject-fmt + additional_dependencies: ["tox>=4.6"] - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v2.7.1" + rev: "v3.0.0-alpha.9-for-vscode" hooks: - id: prettier - additional_dependencies: - - prettier@2.7.1 - - "@prettier/plugin-xml@2.2" args: ["--print-width=120", "--prose-wrap=always"] - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.33.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.0.272" hooks: - - id: markdownlint + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes - - repo: https://github.com/tox-dev/pyproject-fmt - rev: "0.9.2" - hooks: - - id: pyproject-fmt diff --git a/docs/changelog/2588.bugfix.rst b/docs/changelog/2588.bugfix.rst new file mode 100644 index 000000000..3abaec580 --- /dev/null +++ b/docs/changelog/2588.bugfix.rst @@ -0,0 +1,3 @@ +Upgrade embedded wheels: + +* setuptools to ``67.8.0`` from ``67.7.2`` diff --git a/docs/conf.py b/docs/conf.py index ca5344871..cd4f9bfc8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,7 +2,7 @@ import subprocess import sys -from datetime import date, datetime +from datetime import datetime, timezone from pathlib import Path from virtualenv.version import __version__ @@ -11,7 +11,7 @@ name = "virtualenv" version = ".".join(__version__.split(".")[:2]) release = __version__ -copyright = f"2007-{date.today().year}, {company}, PyPA" +copyright = f"2007-{datetime.now(tz=timezone.utc).year}, {company}, PyPA" # noqa: A001 extensions = [ "sphinx.ext.autodoc", @@ -31,7 +31,7 @@ today_fmt = "%B %d, %Y" html_theme = "furo" -html_title, html_last_updated_fmt = project, datetime.now().isoformat() +html_title, html_last_updated_fmt = project, datetime.now(tz=timezone.utc).isoformat() pygments_style, pygments_dark_style = "sphinx", "monokai" html_static_path, html_css_files = ["_static"], ["custom.css"] @@ -52,7 +52,7 @@ def setup(app): root, exe = here.parent, Path(sys.executable) towncrier = exe.with_name(f"towncrier{exe.suffix}") cmd = [str(towncrier), "build", "--draft", "--version", "NEXT"] - new = subprocess.check_output(cmd, cwd=root, text=True, stderr=subprocess.DEVNULL, encoding="UTF-8") + new = subprocess.check_output(cmd, cwd=root, text=True, stderr=subprocess.DEVNULL, encoding="UTF-8") # noqa: S603 (root / "docs" / "_draft.rst").write_text("" if "No significant changes" in new else new, encoding="UTF-8") # the CLI arguments are dynamically generated diff --git a/docs/render_cli.py b/docs/render_cli.py index 76cecd1c5..ecf4ec885 100644 --- a/docs/render_cli.py +++ b/docs/render_cli.py @@ -76,18 +76,14 @@ def a(*args, **kwargs): prev(*args, **kwargs) if key == "activators": return True - elif key == "creator": + if key == "creator": if name == "venv": - from virtualenv.create.via_global_ref.venv import ( - ViaGlobalRefMeta, - ) + from virtualenv.create.via_global_ref.venv import ViaGlobalRefMeta meta = ViaGlobalRefMeta() meta.symlink_error = None return meta - from virtualenv.create.via_global_ref.builtin.via_global_self_do import ( - BuiltinViaGlobalRefMeta, - ) + from virtualenv.create.via_global_ref.builtin.via_global_self_do import BuiltinViaGlobalRefMeta meta = BuiltinViaGlobalRefMeta() meta.symlink_error = None @@ -126,11 +122,16 @@ def build_rows(options): for option in options: names = option["name"] default = option["default"] - if default is not None: - if isinstance(default, str) and default and default[0] == default[-1] and default[0] == '"': - default = default[1:-1] - if default == SUPPRESS: - default = None + if ( + default is not None + and isinstance(default, str) + and default + and default[0] == default[-1] + and default[0] == '"' + ): + default = default[1:-1] + if default == SUPPRESS: + default = None choices = option.get("choices") key = names[0].strip("-") if key in CliTable.plugins: @@ -176,10 +177,7 @@ def _get_targeted_names(self, row): @staticmethod def _get_help_text(row): name = row.names[0] - if name in ("--creator",): - content = row.help[: row.help.index("(") - 1] - else: - content = row.help + content = row.help[: row.help.index("(") - 1] if name in ("--creator",) else row.help help_body = n.paragraph("", "", n.Text(content)) if row.choices is not None: help_body += n.Text("; choice of: ") @@ -207,11 +205,10 @@ def _get_default(row): default_body += n.literal(text="builtin") default_body += n.Text(" if exist, else ") default_body += n.literal(text="venv") + elif default is None: + default_body = n.paragraph("", text="") else: - if default is None: - default_body = n.paragraph("", text="") - else: - default_body = n.literal(text=default if isinstance(default, str) else str(default)) + default_body = n.literal(text=default if isinstance(default, str) else str(default)) return default_body def register_target_option(self, target) -> None: @@ -221,9 +218,9 @@ def register_target_option(self, target) -> None: domain.add_program_option(None, key, self.env.docname, key) -def literal_data(rawtext, app, type, slug, options): # noqa: U100 +def literal_data(rawtext, app, of_type, slug, options): # noqa: ARG001 """Create a link to a BitBucket resource.""" - of_class = type.split(".") + of_class = of_type.split(".") data = getattr(__import__(".".join(of_class[:-1]), fromlist=[of_class[-1]]), of_class[-1]) return [n.literal("", text=",".join(data))], [] diff --git a/pyproject.toml b/pyproject.toml index 2db441002..8b2503984 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.3", - "hatchling>=1.14", + "hatchling>=1.17.1", ] [project] @@ -24,8 +24,13 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", - "Programming Language :: Python :: 3", "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", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries", @@ -37,31 +42,31 @@ dynamic = [ ] dependencies = [ "distlib<1,>=0.3.6", - "filelock<4,>=3.11", - 'importlib-metadata>=6.4.1; python_version < "3.8"', - "platformdirs<4,>=3.2", + "filelock<4,>=3.12", + 'importlib-metadata>=6.6; python_version < "3.8"', + "platformdirs<4,>=3.5.1", ] optional-dependencies.docs = [ - "furo>=2023.3.27", + "furo>=2023.5.20", "proselint>=0.13", - "sphinx>=6.1.3", + "sphinx>=7.0.1", "sphinx-argparse>=0.4", "sphinxcontrib-towncrier>=0.2.1a0", - "towncrier>=22.12", + "towncrier>=23.6", ] optional-dependencies.test = [ "covdefaults>=2.3", - "coverage>=7.2.3", + "coverage>=7.2.7", "coverage-enable-subprocess>=1", "flaky>=3.7", "packaging>=23.1", "pytest>=7.3.1", "pytest-env>=0.8.1", - 'pytest-freezer>=0.4.2; platform_python_implementation == "PyPy"', + 'pytest-freezer>=0.4.6; platform_python_implementation == "PyPy"', "pytest-mock>=3.10", "pytest-randomly>=3.12", "pytest-timeout>=2.1", - "setuptools>=67.7.1", + "setuptools>=67.8", 'time-machine>=2.9; platform_python_implementation == "CPython"', ] urls.Documentation = "https://virtualenv.pypa.io" @@ -98,23 +103,6 @@ version.source = "vcs" [tool.black] line-length = 120 -[tool.isort] -profile = "black" -known_first_party = ["virtualenv"] -add_imports = ["from __future__ import annotations"] - -[tool.flake8] -max-complexity = 22 -max-line-length = 120 -unused-arguments-ignore-abstract-functions = true -noqa-require-code = true -dictionaries = ["en_US", "python", "technical", "django"] -ignore = [ - "E203", # whitespace before ':' - "C901", # is too complex - "W503", # line break before binary operator -] - [tool.pytest.ini_options] markers = ["slow"] timeout = 600 @@ -139,9 +127,6 @@ run.parallel = true run.plugins = ["covdefaults"] run.relative_files = true -[tool.pep8] -max-line-length = "120" - [tool.towncrier] name = "tox" filename = "docs/changelog.rst" @@ -149,3 +134,28 @@ directory = "docs/changelog" title_format = false issue_format = ":issue:`{issue}`" template = "docs/changelog/template.jinja2" + +[tool.ruff] +select = ["ALL"] +line-length = 120 +target-version = "py37" +isort = {known-first-party = ["virtualenv"], required-imports = ["from __future__ import annotations"]} +ignore = [ + "ANN", # no type checking added yet + "D10", # no docstrings + "D40", # no imperative mode for docstrings + "PTH", # no pathlib, <=39 has problems on Windows with absolute/resolve, can revisit once we no longer need 39 + "INP001", # ignore implicit namespace packages + "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible + "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible + "S104", # Possible binding to all interface +] +[tool.ruff.per-file-ignores] +"tests/**/*.py" = [ + "S101", # asserts allowed in tests... + "FBT", # don"t care about booleans as positional arguments in tests + "INP001", # no implicit namespace + "D", # don"t care about documentation in tests + "S603", # `subprocess` call: check for execution of untrusted input + "PLR2004", # Magic value used in comparison, consider replacing with a constant variable +] diff --git a/src/virtualenv/__main__.py b/src/virtualenv/__main__.py index 0ff40d831..ee7341b6e 100644 --- a/src/virtualenv/__main__.py +++ b/src/virtualenv/__main__.py @@ -3,41 +3,41 @@ import logging import os import sys -from datetime import datetime +from timeit import default_timer def run(args=None, options=None, env=None): env = os.environ if env is None else env - start = datetime.now() + start = default_timer() from virtualenv.run import cli_run - from virtualenv.util.error import ProcessCallFailed + from virtualenv.util.error import ProcessCallFailedError if args is None: args = sys.argv[1:] try: session = cli_run(args, options, env) logging.warning(LogSession(session, start)) - except ProcessCallFailed as exception: - print(f"subprocess call failed for {exception.cmd} with code {exception.code}") - print(exception.out, file=sys.stdout, end="") - print(exception.err, file=sys.stderr, end="") - raise SystemExit(exception.code) + except ProcessCallFailedError as exception: + 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 class LogSession: - def __init__(self, session, start): + def __init__(self, session, start) -> None: self.session = session self.start = start - def __str__(self): + def __str__(self) -> str: spec = self.session.creator.interpreter.spec - elapsed = (datetime.now() - self.start).total_seconds() * 1000 + elapsed = (default_timer() - self.start) * 1000 lines = [ f"created virtual environment {spec} in {elapsed:.0f}ms", - f" creator {str(self.session.creator)}", + f" creator {self.session.creator!s}", ] if self.session.seeder.enabled: - lines.append(f" seeder {str(self.session.seeder)}") + lines.append(f" seeder {self.session.seeder!s}") path = self.session.creator.purelib.iterdir() packages = sorted("==".join(i.stem.split("-")) for i in path if i.suffix == ".dist-info") lines.append(f" added seed packages: {', '.join(packages)}") @@ -58,11 +58,10 @@ def run_with_catch(args=None, env=None): try: if getattr(options, "with_traceback", False): raise - else: - if not (isinstance(exception, SystemExit) and exception.code == 0): - logging.error("%s: %s", type(exception).__name__, exception) - code = exception.code if isinstance(exception, SystemExit) else 1 - sys.exit(code) + if not (isinstance(exception, SystemExit) and exception.code == 0): + logging.error("%s: %s", type(exception).__name__, exception) # noqa: TRY400 + code = exception.code if isinstance(exception, SystemExit) else 1 + sys.exit(code) finally: logging.shutdown() # force flush of log messages before the trace is printed diff --git a/src/virtualenv/activation/activator.py b/src/virtualenv/activation/activator.py index ef35b1717..7d41e8ae5 100644 --- a/src/virtualenv/activation/activator.py +++ b/src/virtualenv/activation/activator.py @@ -5,26 +5,28 @@ class Activator(metaclass=ABCMeta): - """Generates activate script for the virtual environment""" + """Generates activate script for the virtual environment.""" - def __init__(self, options): - """Create a new activator generator. + def __init__(self, options) -> None: + """ + Create a new activator generator. :param options: the parsed options as defined within :meth:`add_parser_arguments` """ self.flag_prompt = os.path.basename(os.getcwd()) if options.prompt == "." else options.prompt @classmethod - def supports(cls, interpreter): # noqa: U100 - """Check if the activation script is supported in the given interpreter. + def supports(cls, interpreter): # noqa: ARG003 + """ + Check if the activation script is supported in the given interpreter. :param interpreter: the interpreter we need to support :return: ``True`` if supported, ``False`` otherwise """ return True - @classmethod - def add_parser_arguments(cls, parser, interpreter): # noqa: U100,B027 + @classmethod # noqa: B027 + def add_parser_arguments(cls, parser, interpreter): """ Add CLI arguments for this activation script. @@ -34,7 +36,8 @@ def add_parser_arguments(cls, parser, interpreter): # noqa: U100,B027 @abstractmethod def generate(self, creator): - """Generate activate script for the given creator. + """ + Generate activate script for the given creator. :param creator: the creator (based of :class:`virtualenv.create.creator.Creator`) we used to create this \ virtual environment diff --git a/src/virtualenv/activation/bash/__init__.py b/src/virtualenv/activation/bash/__init__.py index 0267fe43f..5e095ddf0 100644 --- a/src/virtualenv/activation/bash/__init__.py +++ b/src/virtualenv/activation/bash/__init__.py @@ -2,7 +2,7 @@ from pathlib import Path -from ..via_template import ViaTemplateActivator +from virtualenv.activation.via_template import ViaTemplateActivator class BashActivator(ViaTemplateActivator): diff --git a/src/virtualenv/activation/batch/__init__.py b/src/virtualenv/activation/batch/__init__.py index e44500532..a6d58ebb4 100644 --- a/src/virtualenv/activation/batch/__init__.py +++ b/src/virtualenv/activation/batch/__init__.py @@ -2,7 +2,7 @@ import os -from ..via_template import ViaTemplateActivator +from virtualenv.activation.via_template import ViaTemplateActivator class BatchActivator(ViaTemplateActivator): diff --git a/src/virtualenv/activation/cshell/__init__.py b/src/virtualenv/activation/cshell/__init__.py index 9065d99a2..7001f999a 100644 --- a/src/virtualenv/activation/cshell/__init__.py +++ b/src/virtualenv/activation/cshell/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..via_template import ViaTemplateActivator +from virtualenv.activation.via_template import ViaTemplateActivator class CShellActivator(ViaTemplateActivator): diff --git a/src/virtualenv/activation/fish/__init__.py b/src/virtualenv/activation/fish/__init__.py index b6304502b..57f790f47 100644 --- a/src/virtualenv/activation/fish/__init__.py +++ b/src/virtualenv/activation/fish/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..via_template import ViaTemplateActivator +from virtualenv.activation.via_template import ViaTemplateActivator class FishActivator(ViaTemplateActivator): diff --git a/src/virtualenv/activation/nushell/__init__.py b/src/virtualenv/activation/nushell/__init__.py index 2691f70b4..68cd4a3b4 100644 --- a/src/virtualenv/activation/nushell/__init__.py +++ b/src/virtualenv/activation/nushell/__init__.py @@ -1,13 +1,13 @@ from __future__ import annotations -from ..via_template import ViaTemplateActivator +from virtualenv.activation.via_template import ViaTemplateActivator class NushellActivator(ViaTemplateActivator): def templates(self): yield "activate.nu" - def replacements(self, creator, dest_folder): # noqa: U100 + def replacements(self, creator, dest_folder): # noqa: ARG002 return { "__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt, "__VIRTUAL_ENV__": str(creator.dest), diff --git a/src/virtualenv/activation/powershell/__init__.py b/src/virtualenv/activation/powershell/__init__.py index f8de975b4..1f6d0f4e4 100644 --- a/src/virtualenv/activation/powershell/__init__.py +++ b/src/virtualenv/activation/powershell/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..via_template import ViaTemplateActivator +from virtualenv.activation.via_template import ViaTemplateActivator class PowerShellActivator(ViaTemplateActivator): diff --git a/src/virtualenv/activation/python/__init__.py b/src/virtualenv/activation/python/__init__.py index ed220dc4e..3126a39f2 100644 --- a/src/virtualenv/activation/python/__init__.py +++ b/src/virtualenv/activation/python/__init__.py @@ -3,7 +3,7 @@ import os from collections import OrderedDict -from ..via_template import ViaTemplateActivator +from virtualenv.activation.via_template import ViaTemplateActivator class PythonActivator(ViaTemplateActivator): diff --git a/src/virtualenv/activation/python/activate_this.py b/src/virtualenv/activation/python/activate_this.py index 192c796a5..d01bd58e6 100644 --- a/src/virtualenv/activation/python/activate_this.py +++ b/src/virtualenv/activation/python/activate_this.py @@ -1,9 +1,10 @@ -"""Activate virtualenv for current interpreter: +""" +Activate virtualenv for current interpreter: Use exec(open(this_file).read(), {'__file__': this_file}). This can be used when you must use an existing Python interpreter, not the virtualenv bin/python. -""" +""" # noqa: D415 from __future__ import annotations import os @@ -12,14 +13,15 @@ try: abs_file = os.path.abspath(__file__) -except NameError: - raise AssertionError("You must use exec(open(this_file).read(), {'__file__': this_file}))") +except NameError as exc: + msg = "You must use exec(open(this_file).read(), {'__file__': this_file}))" + raise AssertionError(msg) from exc bin_dir = os.path.dirname(abs_file) base = bin_dir[: -len("__BIN_NAME__") - 1] # strip away the bin part from the __file__, plus the path separator # prepend bin to PATH (this file is inside the bin directory) -os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep)) +os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)]) os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory # add the virtual environments libraries to the host python import mechanism diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py index facd705d4..994876c32 100644 --- a/src/virtualenv/activation/via_template.py +++ b/src/virtualenv/activation/via_template.py @@ -27,9 +27,9 @@ def generate(self, creator): generated = self._generate(replacements, self.templates(), dest_folder, creator) if self.flag_prompt is not None: creator.pyenv_cfg["prompt"] = self.flag_prompt - return generated + return generated # noqa: RET504 - def replacements(self, creator, dest_folder): # noqa: U100 + def replacements(self, creator, dest_folder): # noqa: ARG002 return { "__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt, "__VIRTUAL_ENV__": str(creator.dest), @@ -60,12 +60,12 @@ def instantiate_template(self, replacements, template, creator): binary = read_binary(self.__module__, template) text = binary.decode("utf-8", errors="strict") for key, value in replacements.items(): - value = self._repr_unicode(creator, value) - text = text.replace(key, value) + value_uni = self._repr_unicode(creator, value) + text = text.replace(key, value_uni) return text @staticmethod - def _repr_unicode(creator, value): # noqa: U100 + def _repr_unicode(creator, value): # noqa: ARG004 return value # by default, we just let it be unicode diff --git a/src/virtualenv/app_data/__init__.py b/src/virtualenv/app_data/__init__.py index ebff2f32f..148c94183 100644 --- a/src/virtualenv/app_data/__init__.py +++ b/src/virtualenv/app_data/__init__.py @@ -1,6 +1,4 @@ -""" -Application data stored by virtualenv. -""" +"""Application data stored by virtualenv.""" from __future__ import annotations @@ -19,15 +17,15 @@ def _default_app_data_dir(env): key = "VIRTUALENV_OVERRIDE_APP_DATA" if key in env: return env[key] - else: - return user_data_dir(appname="virtualenv", appauthor="pypa") + return user_data_dir(appname="virtualenv", appauthor="pypa") def make_app_data(folder, **kwargs): is_read_only = kwargs.pop("read_only") env = kwargs.pop("env") if kwargs: # py3+ kwonly - raise TypeError("unexpected keywords: {}") + msg = "unexpected keywords: {}" + raise TypeError(msg) if folder is None: folder = _default_app_data_dir(env) @@ -45,9 +43,8 @@ def make_app_data(folder, **kwargs): if os.access(folder, os.W_OK): return AppDataDiskFolder(folder) - else: - logging.debug("app data folder %s has no write access", folder) - return TempAppData() + logging.debug("app data folder %s has no write access", folder) + return TempAppData() __all__ = ( diff --git a/src/virtualenv/app_data/base.py b/src/virtualenv/app_data/base.py index db089311f..4d82e2138 100644 --- a/src/virtualenv/app_data/base.py +++ b/src/virtualenv/app_data/base.py @@ -1,6 +1,4 @@ -""" -Application data stored by virtualenv. -""" +"""Application data stored by virtualenv.""" from __future__ import annotations @@ -11,15 +9,15 @@ class AppData(metaclass=ABCMeta): - """Abstract storage interface for the virtualenv application""" + """Abstract storage interface for the virtualenv application.""" @abstractmethod def close(self): - """called before virtualenv exits""" + """Called before virtualenv exits.""" @abstractmethod def reset(self): - """called when the user passes in the reset app data""" + """Called when the user passes in the reset app data.""" @abstractmethod def py_info(self, path): @@ -51,7 +49,7 @@ def wheel_image(self, for_py_version, name): @contextmanager def ensure_extracted(self, path, to_folder=None): - """Some paths might be within the zipapp, unzip these to a path on the disk""" + """Some paths might be within the zipapp, unzip these to a path on the disk.""" if IS_ZIPAPP: with self.extract(path, to_folder) as result: yield result diff --git a/src/virtualenv/app_data/na.py b/src/virtualenv/app_data/na.py index 9b80ef5e8..921e83a81 100644 --- a/src/virtualenv/app_data/na.py +++ b/src/virtualenv/app_data/na.py @@ -6,45 +6,45 @@ class AppDataDisabled(AppData): - """No application cache available (most likely as we don't have write permissions)""" + """No application cache available (most likely as we don't have write permissions).""" transient = True can_update = False - def __init__(self): + def __init__(self) -> None: pass error = RuntimeError("no app data folder available, probably no write access to the folder") def close(self): - """do nothing""" + """Do nothing.""" def reset(self): - """do nothing""" + """Do nothing.""" - def py_info(self, path): # noqa: U100 + def py_info(self, path): # noqa: ARG002 return ContentStoreNA() - def embed_update_log(self, distribution, for_py_version): # noqa: U100 + def embed_update_log(self, distribution, for_py_version): # noqa: ARG002 return ContentStoreNA() - def extract(self, path, to_folder): # noqa: U100 + def extract(self, path, to_folder): # noqa: ARG002 raise self.error @contextmanager - def locked(self, path): # noqa: U100 - """do nothing""" + def locked(self, path): # noqa: ARG002 + """Do nothing.""" yield @property def house(self): raise self.error - def wheel_image(self, for_py_version, name): # noqa: U100 + def wheel_image(self, for_py_version, name): # noqa: ARG002 raise self.error def py_info_clear(self): - """nothing to clear""" + """Nothing to clear.""" class ContentStoreNA(ContentStore): @@ -52,14 +52,14 @@ def exists(self): return False def read(self): - """nothing to read""" - return None + """Nothing to read.""" + return - def write(self, content): # noqa: U100 - """nothing to write""" + def write(self, content): + """Nothing to write.""" def remove(self): - """nothing to remove""" + """Nothing to remove.""" @contextmanager def locked(self): diff --git a/src/virtualenv/app_data/read_only.py b/src/virtualenv/app_data/read_only.py index 3cec79a97..952dbad5a 100644 --- a/src/virtualenv/app_data/read_only.py +++ b/src/virtualenv/app_data/read_only.py @@ -12,12 +12,14 @@ class ReadOnlyAppData(AppDataDiskFolder): def __init__(self, folder: str) -> None: if not os.path.isdir(folder): - raise RuntimeError(f"read-only app data directory {folder} does not exist") + msg = f"read-only app data directory {folder} does not exist" + raise RuntimeError(msg) super().__init__(folder) self.lock = NoOpFileLock(folder) def reset(self) -> None: - raise RuntimeError("read-only app data does not support reset") + msg = "read-only app data does not support reset" + raise RuntimeError(msg) def py_info_clear(self) -> None: raise NotImplementedError @@ -25,13 +27,14 @@ def py_info_clear(self) -> None: def py_info(self, path): return _PyInfoStoreDiskReadOnly(self.py_info_at, path) - def embed_update_log(self, distribution, for_py_version): # noqa: U100 + def embed_update_log(self, distribution, for_py_version): raise NotImplementedError class _PyInfoStoreDiskReadOnly(PyInfoStoreDisk): - def write(self, content): # noqa: U100 - raise RuntimeError("read-only app data python info cannot be updated") + def write(self, content): # noqa: ARG002 + msg = "read-only app data python info cannot be updated" + raise RuntimeError(msg) __all__ = [ diff --git a/src/virtualenv/app_data/via_disk_folder.py b/src/virtualenv/app_data/via_disk_folder.py index 7abdd1e80..fa87149e2 100644 --- a/src/virtualenv/app_data/via_disk_folder.py +++ b/src/virtualenv/app_data/via_disk_folder.py @@ -20,14 +20,14 @@ ├── py_info.py ├── debug.py └── _virtualenv.py -""" +""" # noqa: D415 from __future__ import annotations import json import logging from abc import ABCMeta -from contextlib import contextmanager +from contextlib import contextmanager, suppress from hashlib import sha256 from virtualenv.util.lock import ReentrantFileLock @@ -39,20 +39,18 @@ class AppDataDiskFolder(AppData): - """ - Store the application data on the disk within a folder layout. - """ + """Store the application data on the disk within a folder layout.""" transient = False can_update = True - def __init__(self, folder): + def __init__(self, folder) -> None: self.lock = ReentrantFileLock(folder) - def __repr__(self): + def __repr__(self) -> str: return f"{type(self).__name__}({self.lock.path})" - def __str__(self): + def __str__(self) -> str: return str(self.lock.path) def reset(self): @@ -60,7 +58,7 @@ def reset(self): safe_delete(self.lock.path) def close(self): - """do nothing""" + """Do nothing.""" @contextmanager def locked(self, path): @@ -70,10 +68,7 @@ def locked(self, path): @contextmanager def extract(self, path, to_folder): - if to_folder is not None: - root = ReentrantFileLock(to_folder()) - else: - root = self.lock / "unzip" / __version__ + root = ReentrantFileLock(to_folder()) if to_folder is not None else self.lock / "unzip" / __version__ with root.lock_for_key(path.name): dest = root.path / path.name if not dest.exists(): @@ -88,7 +83,7 @@ def py_info(self, path): return PyInfoStoreDisk(self.py_info_at, path) def py_info_clear(self): - """ """ + """clear py info.""" py_info_folder = self.py_info_at with py_info_folder: for filename in py_info_folder.path.iterdir(): @@ -111,11 +106,11 @@ def wheel_image(self, for_py_version, name): class JSONStoreDisk(ContentStore, metaclass=ABCMeta): - def __init__(self, in_folder, key, msg, msg_args): + def __init__(self, in_folder, key, msg, msg_args) -> None: self.in_folder = in_folder self.key = key self.msg = msg - self.msg_args = msg_args + (self.file,) + self.msg_args = (*msg_args, self.file) @property def file(self): @@ -128,22 +123,21 @@ def read(self): data, bad_format = None, False try: data = json.loads(self.file.read_text(encoding="utf-8")) - logging.debug(f"got {self.msg} from %s", *self.msg_args) - return data except ValueError: bad_format = True - except Exception: + except Exception: # noqa: BLE001, S110 pass + else: + logging.debug("got %s from %s", self.msg, self.msg_args) + return data if bad_format: - try: + with suppress(OSError): # reading and writing on the same file may cause race on multiple processes self.remove() - except OSError: # reading and writing on the same file may cause race on multiple processes - pass return None def remove(self): self.file.unlink() - logging.debug(f"removed {self.msg} at %s", *self.msg_args) + logging.debug("removed %s at %s", self.msg, self.msg_args) @contextmanager def locked(self): @@ -154,17 +148,17 @@ def write(self, content): folder = self.file.parent folder.mkdir(parents=True, exist_ok=True) self.file.write_text(json.dumps(content, sort_keys=True, indent=2), encoding="utf-8") - logging.debug(f"wrote {self.msg} at %s", *self.msg_args) + logging.debug("wrote %s at %s", self.msg, self.msg_args) class PyInfoStoreDisk(JSONStoreDisk): - def __init__(self, in_folder, path): + def __init__(self, in_folder, path) -> None: key = sha256(str(path).encode("utf-8")).hexdigest() super().__init__(in_folder, key, "python info of %s", (path,)) class EmbedDistributionUpdateStoreDisk(JSONStoreDisk): - def __init__(self, in_folder, distribution): + def __init__(self, in_folder, distribution) -> None: super().__init__( in_folder, distribution, diff --git a/src/virtualenv/app_data/via_tempdir.py b/src/virtualenv/app_data/via_tempdir.py index 8ae86ec2c..0a30dfe1c 100644 --- a/src/virtualenv/app_data/via_tempdir.py +++ b/src/virtualenv/app_data/via_tempdir.py @@ -12,18 +12,18 @@ class TempAppData(AppDataDiskFolder): transient = True can_update = False - def __init__(self): + def __init__(self) -> None: super().__init__(folder=mkdtemp()) logging.debug("created temporary app data folder %s", self.lock.path) def reset(self): - """this is a temporary folder, is already empty to start with""" + """This is a temporary folder, is already empty to start with.""" def close(self): logging.debug("remove temporary app data folder %s", self.lock.path) safe_delete(self.lock.path) - def embed_update_log(self, distribution, for_py_version): # noqa: U100 + def embed_update_log(self, distribution, for_py_version): raise NotImplementedError diff --git a/src/virtualenv/config/cli/parser.py b/src/virtualenv/config/cli/parser.py index 915257b1e..9323d4e81 100644 --- a/src/virtualenv/config/cli/parser.py +++ b/src/virtualenv/config/cli/parser.py @@ -5,13 +5,12 @@ from collections import OrderedDict from virtualenv.config.convert import get_type - -from ..env_var import get_env_var -from ..ini import IniConfig +from virtualenv.config.env_var import get_env_var +from virtualenv.config.ini import IniConfig class VirtualEnvOptions(Namespace): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self._src = None self._sources = {} @@ -22,7 +21,7 @@ def set_src(self, key, value, src): src = "env var" self._sources[key] = src - def __setattr__(self, key, value): + def __setattr__(self, key, value) -> None: if getattr(self, "_src", None) is not None: self._sources[key] = self._src super().__setattr__(key, value) @@ -36,16 +35,14 @@ def verbosity(self): return None return max(self.verbose - self.quiet, 0) - def __repr__(self): + def __repr__(self) -> str: return f"{type(self).__name__}({', '.join(f'{k}={v}' for k, v in vars(self).items() if not k.startswith('_'))})" class VirtualEnvConfigParser(ArgumentParser): - """ - Custom option parser which updates its defaults by checking the configuration files and environmental variables - """ + """Custom option parser which updates its defaults by checking the configuration files and environmental vars.""" - def __init__(self, options=None, env=None, *args, **kwargs): + def __init__(self, options=None, env=None, *args, **kwargs) -> None: env = os.environ if env is None else env self.file_config = IniConfig(env) self.epilog_list = [] @@ -57,7 +54,8 @@ def __init__(self, options=None, env=None, *args, **kwargs): super().__init__(*args, **kwargs) self._fixed = set() if options is not None and not isinstance(options, VirtualEnvOptions): - raise TypeError("options must be of type VirtualEnvOptions") + msg = "options must be of type VirtualEnvOptions" + raise TypeError(msg) self.options = VirtualEnvOptions() if options is None else options self._interpreter = None self._app_data = None @@ -97,18 +95,19 @@ def parse_known_args(self, args=None, namespace=None): if namespace is None: namespace = self.options elif namespace is not self.options: - raise ValueError("can only pass in parser.options") + msg = "can only pass in parser.options" + raise ValueError(msg) self._fix_defaults() - self.options._src = "cli" + self.options._src = "cli" # noqa: SLF001 try: namespace.env = self.env return super().parse_known_args(args, namespace=namespace) finally: - self.options._src = None + self.options._src = None # noqa: SLF001 class HelpFormatter(ArgumentDefaultsHelpFormatter): - def __init__(self, prog): + def __init__(self, prog) -> None: super().__init__(prog, max_help_position=32, width=240) def _get_help_string(self, action): diff --git a/src/virtualenv/config/convert.py b/src/virtualenv/config/convert.py index e5f1ae8f5..a97745608 100644 --- a/src/virtualenv/config/convert.py +++ b/src/virtualenv/config/convert.py @@ -5,11 +5,11 @@ class TypeData: - def __init__(self, default_type, as_type): + def __init__(self, default_type, as_type) -> None: self.default_type = default_type self.as_type = as_type - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(base={self.default_type}, as={self.as_type})" def convert(self, value): @@ -30,7 +30,8 @@ class BoolType(TypeData): def convert(self, value): if value.lower() not in self.BOOLEAN_STATES: - raise ValueError(f"Not a boolean: {value}") + msg = f"Not a boolean: {value}" + raise ValueError(msg) return self.BOOLEAN_STATES[value.lower()] @@ -43,19 +44,19 @@ def convert(self, value): class ListType(TypeData): def _validate(self): - """ """ + """no op.""" - def convert(self, value, flatten=True): # noqa: U100 + def convert(self, value, flatten=True): # noqa: ARG002, FBT002 values = self.split_values(value) result = [] for value in values: sub_values = value.split(os.pathsep) result.extend(sub_values) - converted = [self.as_type(i) for i in result] - return converted + return [self.as_type(i) for i in result] def split_values(self, value): - """Split the provided value into a list. + """ + Split the provided value into a list. First this is done by newlines. If there were no newlines in the text, then we next try to split by comma. @@ -75,7 +76,7 @@ def split_values(self, value): def convert(value, as_type, source): - """Convert the value as a given type where the value comes from the given source""" + """Convert the value as a given type where the value comes from the given source.""" try: return as_type.convert(value) except Exception as exception: diff --git a/src/virtualenv/config/env_var.py b/src/virtualenv/config/env_var.py index c6549e2da..e12723471 100644 --- a/src/virtualenv/config/env_var.py +++ b/src/virtualenv/config/env_var.py @@ -1,10 +1,13 @@ from __future__ import annotations +from contextlib import suppress + from .convert import convert def get_env_var(key, as_type, env): - """Get the environment variable option. + """ + Get the environment variable option. :param key: the config key requested :param as_type: the type we would like to convert it to @@ -15,12 +18,11 @@ def get_env_var(key, as_type, env): if env.get(environ_key): value = env[environ_key] - try: + with suppress(Exception): # note the converter already logs a warning when failures happen source = f"env var {environ_key}" as_type = convert(value, as_type, source) return as_type, source - except Exception: # note the converter already logs a warning when failures happen - pass + return None __all__ = [ diff --git a/src/virtualenv/config/ini.py b/src/virtualenv/config/ini.py index c56665c32..e333eefba 100644 --- a/src/virtualenv/config/ini.py +++ b/src/virtualenv/config/ini.py @@ -16,7 +16,7 @@ class IniConfig: section = "virtualenv" - def __init__(self, env=None): + def __init__(self, env=None) -> None: env = os.environ if env is None else env config_file = env.get(self.VIRTUALENV_CONFIG_FILE_ENV_VAR, None) self.is_env_var = config_file is not None @@ -40,7 +40,7 @@ def __init__(self, env=None): try: self._load() self.has_virtualenv_section = self.config_parser.has_section(self.section) - except Exception as exc: + except Exception as exc: # noqa: BLE001 exception = exc if exception is not None: logging.error("failed to read config file %s because %r", config_file, exception) @@ -58,12 +58,12 @@ def get(self, key, as_type): raw_value = self.config_parser.get(self.section, key.lower()) value = convert(raw_value, as_type, source) result = value, source - except Exception: + except Exception: # noqa: BLE001 result = None self._cache[cache_key] = result return result - def __bool__(self): + def __bool__(self) -> bool: return bool(self.has_config_file) and bool(self.has_virtualenv_section) @property diff --git a/src/virtualenv/create/creator.py b/src/virtualenv/create/creator.py index 12d7424ad..8ff54166e 100644 --- a/src/virtualenv/create/creator.py +++ b/src/virtualenv/create/creator.py @@ -22,15 +22,16 @@ class CreatorMeta: - def __init__(self): + def __init__(self) -> None: self.error = None class Creator(metaclass=ABCMeta): - """A class that given a python Interpreter creates a virtual environment""" + """A class that given a python Interpreter creates a virtual environment.""" - def __init__(self, options, interpreter): - """Construct a new virtual environment creator. + def __init__(self, options, interpreter) -> None: + """ + Construct a new virtual environment creator. :param options: the CLI option as parsed from :meth:`add_parser_arguments` :param interpreter: the interpreter to create virtual environment from @@ -44,7 +45,7 @@ def __init__(self, options, interpreter): self.app_data = options.app_data self.env = options.env - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in self._args())})" def _args(self): @@ -55,8 +56,9 @@ def _args(self): ] @classmethod - def can_create(cls, interpreter): # noqa: U100 - """Determine if we can create a virtual environment. + def can_create(cls, interpreter): # noqa: ARG003 + """ + Determine if we can create a virtual environment. :param interpreter: the interpreter in question :return: ``None`` if we can't create, any other object otherwise that will be forwarded to \ @@ -65,8 +67,9 @@ def can_create(cls, interpreter): # noqa: U100 return True @classmethod - def add_parser_arguments(cls, parser, interpreter, meta, app_data): # noqa: U100 - """Add CLI arguments for the creator. + def add_parser_arguments(cls, parser, interpreter, meta, app_data): # noqa: ARG003 + """ + Add CLI arguments for the creator. :param parser: the CLI parser :param app_data: the application data folder @@ -99,12 +102,13 @@ def create(self): raise NotImplementedError @classmethod - def validate_dest(cls, raw_value): - """No path separator in the path, valid chars and must be write-able""" + def validate_dest(cls, raw_value): # noqa: C901 + """No path separator in the path, valid chars and must be write-able.""" def non_write_able(dest, value): common = Path(*os.path.commonprefix([value.parts, dest.parts])) - raise ArgumentTypeError(f"the destination {dest.relative_to(common)} is not write-able at {common}") + msg = f"the destination {dest.relative_to(common)} is not write-able at {common}" + raise ArgumentTypeError(msg) # the file system must be able to encode # note in newer CPython this is always utf-8 https://www.python.org/dev/peps/pep-0529/ @@ -116,7 +120,7 @@ def non_write_able(dest, value): trip = char.encode(encoding, **kwargs).decode(encoding) if trip == char: continue - raise ValueError(trip) + raise ValueError(trip) # noqa: TRY301 except ValueError: refused[char] = None if refused: @@ -124,20 +128,23 @@ def non_write_able(dest, value): msg = f"the file system codec ({encoding}) cannot handle characters {bad!r} within {raw_value!r}" raise ArgumentTypeError(msg) if os.pathsep in raw_value: - msg = f"destination {raw_value!r} must not contain the path separator ({os.pathsep})" - raise ArgumentTypeError(f"{msg} as this would break the activation scripts") + msg = ( + f"destination {raw_value!r} must not contain the path separator ({os.pathsep})" + f" as this would break the activation scripts" + ) + raise ArgumentTypeError(msg) value = Path(raw_value) if value.exists() and value.is_file(): - raise ArgumentTypeError(f"the destination {value} already exists and is a file") + msg = f"the destination {value} already exists and is a file" + raise ArgumentTypeError(msg) dest = Path(os.path.abspath(str(value))).resolve() # on Windows absolute does not imply resolve so use both value = dest while dest: if dest.exists(): if os.access(str(dest), os.W_OK): break - else: - non_write_able(dest, value) + non_write_able(dest, value) base, _ = dest.parent, dest.name if base == dest: non_write_able(dest, value) # pragma: no cover @@ -174,9 +181,7 @@ def setup_ignore_vcs(self): @property def debug(self): - """ - :return: debug information about the virtual environment (only valid after :meth:`create` has run) - """ + """:return: debug information about the virtual environment (only valid after :meth:`create` has run)""" if self._debug is None and self.exe is not None: self._debug = get_env_debug_info(self.exe, self.debug_script(), self.app_data, self.env) return self._debug @@ -200,15 +205,15 @@ def get_env_debug_info(env_exe, debug_script, app_data, env): if out: result = literal_eval(out) else: - if code == 2 and "file" in err: + if code == 2 and "file" in err: # noqa: PLR2004 # Re-raise FileNotFoundError from `run_cmd()` - raise OSError(err) - raise Exception(err) + raise OSError(err) # noqa: TRY301 + raise Exception(err) # noqa: TRY002, TRY301 else: result = json.loads(out) if err: result["err"] = err - except Exception as exception: + except Exception as exception: # noqa: BLE001 return {"out": out, "err": err, "returncode": code, "exception": repr(exception)} if "sys" in result and "path" in result["sys"]: del result["sys"]["path"][0] diff --git a/src/virtualenv/create/debug.py b/src/virtualenv/create/debug.py index a922fb14a..ee7fc900a 100644 --- a/src/virtualenv/create/debug.py +++ b/src/virtualenv/create/debug.py @@ -1,4 +1,4 @@ -"""Inspect a target Python interpreter virtual environment wise""" +"""Inspect a target Python interpreter virtual environment wise.""" from __future__ import annotations import sys # built-in @@ -8,10 +8,7 @@ def encode_path(value): if value is None: return None if not isinstance(value, (str, bytes)): - if isinstance(value, type): - value = repr(value) - else: - value = repr(type(value)) + value = repr(value) if isinstance(value, type) else repr(type(value)) if isinstance(value, bytes): value = value.decode(sys.getfilesystemencoding()) return value @@ -21,13 +18,13 @@ def encode_list_path(value): return [encode_path(i) for i in value] -def run(): - """print debug data about the virtual environment""" +def run(): # noqa: PLR0912 + """Print debug data about the virtual environment.""" try: from collections import OrderedDict except ImportError: # pragma: no cover # this is possible if the standard library cannot be accessed - # noinspection PyPep8Naming + OrderedDict = dict # pragma: no cover # noqa: N806 result = OrderedDict([("sys", OrderedDict())]) path_keys = ( @@ -43,10 +40,7 @@ def run(): ) for key in path_keys: value = getattr(sys, key, None) - if isinstance(value, list): - value = encode_list_path(value) - else: - value = encode_path(value) + value = encode_list_path(value) if isinstance(value, list) else encode_path(value) result["sys"][key] = value result["sys"]["fs_encoding"] = sys.getfilesystemencoding() result["sys"]["io_encoding"] = getattr(sys.stdout, "encoding", None) @@ -66,7 +60,6 @@ def run(): result["os"] = repr(os) try: - # noinspection PyUnresolvedReferences import site # site result["site"] = repr(site) @@ -74,7 +67,6 @@ def run(): result["site"] = repr(exception) # pragma: no cover try: - # noinspection PyUnresolvedReferences import datetime # site result["datetime"] = repr(datetime) @@ -82,7 +74,6 @@ def run(): result["datetime"] = repr(exception) # pragma: no cover try: - # noinspection PyUnresolvedReferences import math # site result["math"] = repr(math) @@ -103,7 +94,7 @@ def run(): except (ValueError, TypeError) as exception: # pragma: no cover sys.stderr.write(repr(exception)) sys.stdout.write(repr(result)) # pragma: no cover - raise SystemExit(1) # pragma: no cover + raise SystemExit(1) # noqa: TRY200, B904 # pragma: no cover if __name__ == "__main__": diff --git a/src/virtualenv/create/describe.py b/src/virtualenv/create/describe.py index e39afe62f..7167e56d7 100644 --- a/src/virtualenv/create/describe.py +++ b/src/virtualenv/create/describe.py @@ -8,11 +8,11 @@ class Describe(metaclass=ABCMeta): - """Given a host interpreter tell us information about what the created interpreter might look like""" + """Given a host interpreter tell us information about what the created interpreter might look like.""" suffix = ".exe" if IS_WIN else "" - def __init__(self, dest, interpreter): + def __init__(self, dest, interpreter) -> None: self.interpreter = interpreter self.dest = dest self._stdlib = None @@ -63,8 +63,8 @@ def _calc_config_vars(self, to): return {k: (to if v is not None and v.startswith(self.interpreter.prefix) else v) for k, v in sys_vars.items()} @classmethod - def can_describe(cls, interpreter): # noqa: U100 - """Knows means it knows how the output will look""" + def can_describe(cls, interpreter): # noqa: ARG003 + """Knows means it knows how the output will look.""" return True @property @@ -77,7 +77,7 @@ def exe(self): @classmethod def exe_stem(cls): - """executable name without suffix - there seems to be no standard way to get this without creating it""" + """Executable name without suffix - there seems to be no standard way to get this without creating it.""" raise NotImplementedError def script(self, name): @@ -87,7 +87,7 @@ def script(self, name): class Python3Supports(Describe, metaclass=ABCMeta): @classmethod def can_describe(cls, interpreter): - return interpreter.version_info.major == 3 and super().can_describe(interpreter) + return interpreter.version_info.major == 3 and super().can_describe(interpreter) # noqa: PLR2004 class PosixSupports(Describe, metaclass=ABCMeta): diff --git a/src/virtualenv/create/pyenv_cfg.py b/src/virtualenv/create/pyenv_cfg.py index 44919abff..03ebfbbda 100644 --- a/src/virtualenv/create/pyenv_cfg.py +++ b/src/virtualenv/create/pyenv_cfg.py @@ -5,7 +5,7 @@ class PyEnvCfg: - def __init__(self, content, path): + def __init__(self, content, path) -> None: self.content = content self.path = path @@ -42,20 +42,20 @@ def refresh(self): self.content = self._read_values(self.path) return self.content - def __setitem__(self, key, value): + def __setitem__(self, key, value) -> None: self.content[key] = value def __getitem__(self, key): return self.content[key] - def __contains__(self, item): + def __contains__(self, item) -> bool: return item in self.content def update(self, other): self.content.update(other) return self - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(path={self.path})" diff --git a/src/virtualenv/create/via_global_ref/_virtualenv.py b/src/virtualenv/create/via_global_ref/_virtualenv.py index 90c32dfba..e328be6a3 100644 --- a/src/virtualenv/create/via_global_ref/_virtualenv.py +++ b/src/virtualenv/create/via_global_ref/_virtualenv.py @@ -1,9 +1,10 @@ -"""Patches that are applied at runtime to the virtual environment""" +"""Patches that are applied at runtime to the virtual environment.""" from __future__ import annotations import os import sys +from contextlib import suppress VIRTUALENV_PATCH_FILE = os.path.join(__file__) @@ -11,10 +12,10 @@ def patch_dist(dist): """ Distutils allows user to configure some arguments via a configuration file: - https://docs.python.org/3/install/index.html#distutils-configuration-files + https://docs.python.org/3/install/index.html#distutils-configuration-files. Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up. - """ + """ # noqa: D205 # we cannot allow some install config as that would get packages installed outside of the virtual environment old_parse_config_files = dist.Distribution.parse_config_files @@ -40,7 +41,7 @@ def parse_config_files(self, *args, **kwargs): class _Finder: - """A meta path finder that allows patching the imported distutils modules""" + """A meta path finder that allows patching the imported distutils modules.""" fullname = None @@ -49,7 +50,7 @@ class _Finder: # See https://github.com/pypa/virtualenv/issues/1895 for details. lock = [] - def find_spec(self, fullname, path, target=None): # noqa: U100 + def find_spec(self, fullname, path, target=None): # noqa: ARG002 if fullname in _DISTUTILS_PATCH and self.fullname is None: # initialize lock[0] lazily if len(self.lock) == 0: @@ -77,13 +78,12 @@ def find_spec(self, fullname, path, target=None): # noqa: U100 old = getattr(spec.loader, func_name) func = self.exec_module if is_new_api else self.load_module if old is not func: - try: + with suppress(AttributeError): # C-Extension loaders are r/o such as zipimporter with <3.7 setattr(spec.loader, func_name, partial(func, old)) - except AttributeError: - pass # C-Extension loaders are r/o such as zipimporter with None: super().__init__() self.copy_error = None self.symlink_error = None @@ -28,7 +27,7 @@ def can_symlink(self): class ViaGlobalRefApi(Creator, metaclass=ABCMeta): - def __init__(self, options, interpreter): + def __init__(self, options, interpreter) -> None: super().__init__(options, interpreter) self.symlinks = self._should_symlink(options) self.enable_system_site_package = options.system_site @@ -62,7 +61,8 @@ def add_parser_arguments(cls, parser, interpreter, meta, app_data): ) group = parser.add_mutually_exclusive_group() if not meta.can_symlink and not meta.can_copy: - raise RuntimeError("neither symlink or copy method supported") + msg = "neither symlink or copy method supported" + raise RuntimeError(msg) if meta.can_symlink: group.add_argument( "--symlinks", @@ -95,13 +95,13 @@ def install_patch(self): dest_path.write_text(text, encoding="utf-8") def env_patch_text(self): - """Patch the distutils package to not be derailed by its configuration files""" + """Patch the distutils package to not be derailed by its configuration files.""" with self.app_data.ensure_extracted(Path(__file__).parent / "_virtualenv.py") as resolved_path: text = resolved_path.read_text(encoding="utf-8") return text.replace('"__SCRIPT_DIR__"', repr(os.path.relpath(str(self.script_dir), str(self.purelib)))) def _args(self): - return super()._args() + [("global", self.enable_system_site_package)] + return [*super()._args(), ("global", self.enable_system_site_package)] def set_pyenv_cfg(self): super().set_pyenv_cfg() diff --git a/src/virtualenv/create/via_global_ref/builtin/builtin_way.py b/src/virtualenv/create/via_global_ref/builtin/builtin_way.py index 5705a7b53..bb520a354 100644 --- a/src/virtualenv/create/via_global_ref/builtin/builtin_way.py +++ b/src/virtualenv/create/via_global_ref/builtin/builtin_way.py @@ -7,9 +7,9 @@ class VirtualenvBuiltin(Creator, Describe, metaclass=ABCMeta): - """A creator that does operations itself without delegation, if we can create it we can also describe it""" + """A creator that does operations itself without delegation, if we can create it we can also describe it.""" - def __init__(self, options, interpreter): + def __init__(self, options, interpreter) -> None: Creator.__init__(self, options, interpreter) Describe.__init__(self, self.dest, interpreter) 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 f7046217c..7c7abd5b4 100644 --- a/src/virtualenv/create/via_global_ref/builtin/cpython/common.py +++ b/src/virtualenv/create/via_global_ref/builtin/cpython/common.py @@ -6,8 +6,7 @@ from virtualenv.create.describe import PosixSupports, WindowsSupports from virtualenv.create.via_global_ref.builtin.ref import RefMust, RefWhen - -from ..via_global_self_do import ViaGlobalRefVirtualenvBuiltin +from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin class CPython(ViaGlobalRefVirtualenvBuiltin, metaclass=ABCMeta): @@ -21,7 +20,7 @@ def exe_stem(cls): class CPythonPosix(CPython, PosixSupports, metaclass=ABCMeta): - """Create a CPython virtual environment on POSIX platforms""" + """Create a CPython virtual environment on POSIX platforms.""" @classmethod def _executables(cls, interpreter): @@ -38,7 +37,7 @@ def _executables(cls, interpreter): # - https://bugs.python.org/issue42013 # - venv host = cls.host_python(interpreter) - for path in (host.parent / n for n in {"python.exe", host.name}): + for path in (host.parent / n for n in {"python.exe", host.name}): # noqa: PLC0208 yield host, [path.name], RefMust.COPY, RefWhen.ANY # for more info on pythonw.exe see https://stackoverflow.com/a/30313091 python_w = host.parent / "pythonw.exe" 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 3d9a5c41d..0b7b023f6 100644 --- a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py +++ b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py @@ -15,7 +15,7 @@ class CPython3(CPython, Python3Supports, metaclass=abc.ABCMeta): - """CPython 3 or later""" + """CPython 3 or later.""" class CPython3Posix(CPythonPosix, CPython3): @@ -43,7 +43,7 @@ def pyvenv_launch_patch_active(cls, interpreter): class CPython3Windows(CPythonWindows, CPython3): - """ """ + """CPython 3 on Windows.""" @classmethod def setup_meta(cls, interpreter): @@ -69,7 +69,7 @@ def executables(cls, interpreter): @classmethod def has_shim(cls, interpreter): - return interpreter.version_info.minor >= 7 and cls.shim(interpreter) is not None + return interpreter.version_info.minor >= 7 and cls.shim(interpreter) is not None # noqa: PLR2004 @classmethod def shim(cls, interpreter): @@ -114,7 +114,7 @@ def python_zip(cls, interpreter): "python{VERSION}.zip" and "python{VERSION}._pth" files. User can move/rename *zip* file and edit `sys.path` by editing *_pth* file. Here the `pattern` is used only for the default *zip* file name! - """ + """ # noqa: D205 pattern = f"*python{interpreter.version_nodot}.zip" matches = fnmatch.filter(interpreter.path, pattern) matched_paths = map(Path, matches) 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 8fa09aa21..598abfaa8 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 @@ -1,4 +1,4 @@ -"""The Apple Framework builds require their own customization""" +"""The Apple Framework builds require their own customization.""" from __future__ import annotations import logging @@ -31,13 +31,12 @@ def create(self): target = self.desired_mach_o_image_path() current = self.current_mach_o_image_path() for src in self._sources: - if isinstance(src, ExePathRefToDest): - if src.must == RefMust.COPY or not self.symlinks: - exes = [self.bin_dir / src.base] - if not self.symlinks: - exes.extend(self.bin_dir / a for a in src.aliases) - for exe in exes: - fix_mach_o(str(exe), current, target, self.interpreter.max_size) + if isinstance(src, ExePathRefToDest) and (src.must == RefMust.COPY or not self.symlinks): + exes = [self.bin_dir / src.base] + if not self.symlinks: + exes.extend(self.bin_dir / a for a in src.aliases) + for exe in exes: + fix_mach_o(str(exe), current, target, self.interpreter.max_size) @classmethod def _executables(cls, interpreter): @@ -70,7 +69,7 @@ def sources(cls, interpreter): # add a symlink to the host python image exe = Path(interpreter.prefix) / "Python3" - yield PathRefToDest(exe, dest=lambda self, _: self.dest / ".Python", must=RefMust.SYMLINK) # noqa: U101 + yield PathRefToDest(exe, dest=lambda self, _: self.dest / ".Python", must=RefMust.SYMLINK) @property def reload_code(self): @@ -92,7 +91,7 @@ def reload_code(self): def fix_mach_o(exe, current, new, max_size): """ - https://en.wikipedia.org/wiki/Mach-O + https://en.wikipedia.org/wiki/Mach-O. Mach-O, short for Mach object file format, is a file format for executables, object code, shared libraries, dynamically-loaded code, and core dumps. A replacement for the a.out format, Mach-O offers more extensibility and @@ -117,17 +116,17 @@ def fix_mach_o(exe, current, new, max_size): try: logging.debug("change Mach-O for %s from %s to %s", exe, current, new) _builtin_change_mach_o(max_size)(exe, current, new) - except Exception as e: - logging.warning("Could not call _builtin_change_mac_o: %s. " "Trying to call install_name_tool instead.", e) + except Exception as e: # noqa: BLE001 + logging.warning("Could not call _builtin_change_mac_o: %s. Trying to call install_name_tool instead.", e) try: cmd = ["install_name_tool", "-change", current, new, exe] - subprocess.check_call(cmd) + subprocess.check_call(cmd) # noqa: S603 except Exception: - logging.fatal("Could not call install_name_tool -- you must " "have Apple's development tools installed") + logging.fatal("Could not call install_name_tool -- you must have Apple's development tools installed") raise -def _builtin_change_mach_o(maxint): +def _builtin_change_mach_o(maxint): # noqa: C901 MH_MAGIC = 0xFEEDFACE # noqa: N806 MH_CIGAM = 0xCEFAEDFE # noqa: N806 MH_MAGIC_64 = 0xFEEDFACF # noqa: N806 @@ -140,16 +139,16 @@ def _builtin_change_mach_o(maxint): class FileView: """A proxy for file-like objects that exposes a given view of a file. Modified from macholib.""" - def __init__(self, file_obj, start=0, size=maxint): + def __init__(self, file_obj, start=0, size=maxint) -> None: if isinstance(file_obj, FileView): - self._file_obj = file_obj._file_obj + self._file_obj = file_obj._file_obj # noqa: SLF001 else: self._file_obj = file_obj self._start = start self._end = start + size self._pos = 0 - def __repr__(self): + def __repr__(self) -> str: return f"" def tell(self): @@ -169,7 +168,8 @@ def seek(self, offset, whence=0): elif whence == os.SEEK_END: seek_to += self._end else: - raise OSError(f"Invalid whence argument to seek: {whence!r}") + msg = f"Invalid whence argument to seek: {whence!r}" + raise OSError(msg) self._checkwindow(seek_to, "seek") self._file_obj.seek(seek_to) self._pos = seek_to - self._start @@ -183,7 +183,7 @@ def write(self, content): self._pos += len(content) def read(self, size=maxint): - assert size >= 0 + assert size >= 0 # noqa: S101 here = self._start + self._pos self._checkwindow(here, "read") size = min(size, self._end - here) @@ -199,15 +199,17 @@ def read_data(file, endian, num=1): return res[0] return res - def mach_o_change(at_path, what, value): - """Replace a given name (what) in any LC_LOAD_DYLIB command found in the given binary with a new name (value), - provided it's shorter.""" + def mach_o_change(at_path, what, value): # noqa: C901 + """ + Replace a given name (what) in any LC_LOAD_DYLIB command found in the given binary with a new name (value), + provided it's shorter. + """ # noqa: D205 def do_macho(file, bits, endian): # Read Mach-O header (the magic number is assumed read by the caller) cpu_type, cpu_sub_type, file_type, n_commands, size_of_commands, flags = read_data(file, endian, 6) # 64-bits header has one more field. - if bits == 64: + if bits == 64: # noqa: PLR2004 read_data(file, endian) # The header is followed by n commands for _ in range(n_commands): @@ -249,7 +251,7 @@ def do_file(file, offset=0, size=maxint): elif magic == MH_CIGAM_64: do_macho(file, 64, LITTLE_ENDIAN) - assert len(what) >= len(value) + assert len(what) >= len(value) # noqa: S101 with open(at_path, "r+b") as f: do_file(f) diff --git a/src/virtualenv/create/via_global_ref/builtin/pypy/common.py b/src/virtualenv/create/via_global_ref/builtin/pypy/common.py index f695d6e95..c7f91e32b 100644 --- a/src/virtualenv/create/via_global_ref/builtin/pypy/common.py +++ b/src/virtualenv/create/via_global_ref/builtin/pypy/common.py @@ -4,8 +4,7 @@ from pathlib import Path from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefMust, RefWhen - -from ..via_global_self_do import ViaGlobalRefVirtualenvBuiltin +from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin class PyPy(ViaGlobalRefVirtualenvBuiltin, metaclass=abc.ABCMeta): @@ -45,7 +44,7 @@ def _add_shared_libs(cls, interpreter): yield from cls._shared_libs(python_dir) @classmethod - def _shared_libs(cls, python_dir): # noqa: U100 + def _shared_libs(cls, python_dir): raise NotImplementedError diff --git a/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py b/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py index b06ecea8a..39f0ed5bb 100644 --- a/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py +++ b/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py @@ -20,7 +20,7 @@ def exe_names(cls, interpreter): class PyPy3Posix(PyPy3, PosixSupports): - """PyPy 3 on POSIX""" + """PyPy 3 on POSIX.""" @classmethod def _shared_libs(cls, python_dir): @@ -55,11 +55,11 @@ def sources(cls, interpreter): class Pypy3Windows(PyPy3, WindowsSupports): - """PyPy 3 on Windows""" + """PyPy 3 on Windows.""" @property def less_v37(self): - return self.interpreter.version_info.minor < 7 + return self.interpreter.version_info.minor < 7 # noqa: PLR2004 @classmethod def _shared_libs(cls, python_dir): diff --git a/src/virtualenv/create/via_global_ref/builtin/ref.py b/src/virtualenv/create/via_global_ref/builtin/ref.py index 19210ad3a..d3dca5d09 100644 --- a/src/virtualenv/create/via_global_ref/builtin/ref.py +++ b/src/virtualenv/create/via_global_ref/builtin/ref.py @@ -2,7 +2,7 @@ Virtual environments in the traditional sense are built as reference to the host python. This file allows declarative references to elements on the file system, allowing our system to automatically detect what modes it can support given the constraints: e.g. can the file system symlink, can the files be read, executed, etc. -""" +""" # noqa: D205 from __future__ import annotations @@ -28,12 +28,12 @@ class RefWhen: class PathRef(metaclass=ABCMeta): - """Base class that checks if a file reference can be symlink/copied""" + """Base class that checks if a file reference can be symlink/copied.""" FS_SUPPORTS_SYMLINK = fs_supports_symlink() FS_CASE_SENSITIVE = fs_is_case_sensitive() - def __init__(self, src, must=RefMust.NA, when=RefWhen.ANY): + def __init__(self, src, must=RefMust.NA, when=RefWhen.ANY) -> None: self.must = must self.when = when self.src = src @@ -45,7 +45,7 @@ def __init__(self, src, must=RefMust.NA, when=RefWhen.ANY): self._can_copy = None if self.exists else False self._can_symlink = None if self.exists else False - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(src={self.src})" @property @@ -92,9 +92,9 @@ def method(self, symlinks): class ExePathRef(PathRef, metaclass=ABCMeta): - """Base class that checks if a executable can be references via symlink/copy""" + """Base class that checks if a executable can be references via symlink/copy.""" - def __init__(self, src, must=RefMust.NA, when=RefWhen.ANY): + def __init__(self, src, must=RefMust.NA, when=RefWhen.ANY) -> None: super().__init__(src, must, when) self._can_run = None @@ -118,9 +118,9 @@ def can_run(self): class PathRefToDest(PathRef): - """Link a path on the file system""" + """Link a path on the file system.""" - def __init__(self, src, dest, must=RefMust.NA, when=RefWhen.ANY): + def __init__(self, src, dest, must=RefMust.NA, when=RefWhen.ANY) -> None: super().__init__(src, must, when) self.dest = dest @@ -135,9 +135,9 @@ def run(self, creator, symlinks): class ExePathRefToDest(PathRefToDest, ExePathRef): - """Link a exe path on the file system""" + """Link a exe path on the file system.""" - def __init__(self, src, targets, dest, must=RefMust.NA, when=RefWhen.ANY): + def __init__(self, src, targets, dest, must=RefMust.NA, when=RefWhen.ANY) -> None: # noqa: PLR0913 ExePathRef.__init__(self, src, must, when) PathRefToDest.__init__(self, src, dest, must, when) if not self.FS_CASE_SENSITIVE: @@ -164,7 +164,7 @@ def run(self, creator, symlinks): if not symlinks: make_exe(link_file) - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(src={self.src}, alias={self.aliases})" diff --git a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py index ccf4289e6..3c0f9cf8c 100644 --- a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py +++ b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py @@ -2,6 +2,7 @@ from abc import ABCMeta +from virtualenv.create.via_global_ref.api import ViaGlobalRefApi, ViaGlobalRefMeta from virtualenv.create.via_global_ref.builtin.ref import ( ExePathRefToDest, RefMust, @@ -9,24 +10,23 @@ ) from virtualenv.util.path import ensure_dir -from ..api import ViaGlobalRefApi, ViaGlobalRefMeta from .builtin_way import VirtualenvBuiltin class BuiltinViaGlobalRefMeta(ViaGlobalRefMeta): - def __init__(self): + def __init__(self) -> None: super().__init__() self.sources = [] class ViaGlobalRefVirtualenvBuiltin(ViaGlobalRefApi, VirtualenvBuiltin, metaclass=ABCMeta): - def __init__(self, options, interpreter): + def __init__(self, options, interpreter) -> None: super().__init__(options, interpreter) self._sources = getattr(options.meta, "sources", None) # if we're created as a describer this might be missing @classmethod def can_create(cls, interpreter): - """By default, all built-in methods assume that if we can describe it we can create it""" + """By default, all built-in methods assume that if we can describe it we can create it.""" # first we must be able to describe it if not cls.can_describe(interpreter): return None @@ -58,7 +58,7 @@ def _sources_can_be_applied(cls, interpreter, meta): meta.sources.append(src) @classmethod - def setup_meta(cls, interpreter): # noqa: U100 + def setup_meta(cls, interpreter): # noqa: ARG003 return BuiltinViaGlobalRefMeta() @classmethod @@ -70,7 +70,7 @@ def to_bin(self, src): return self.bin_dir / src.name @classmethod - def _executables(cls, interpreter): # noqa: U100 + def _executables(cls, interpreter): raise NotImplementedError def create(self): @@ -104,8 +104,8 @@ def ensure_directories(self): def set_pyenv_cfg(self): """ We directly inject the base prefix and base exec prefix to avoid site.py needing to discover these - from home (which usually is done within the interpreter itself) - """ + from home (which usually is done within the interpreter itself). + """ # noqa: D205 super().set_pyenv_cfg() self.pyenv_cfg["base-prefix"] = self.interpreter.system_prefix self.pyenv_cfg["base-exec-prefix"] = self.interpreter.system_exec_prefix diff --git a/src/virtualenv/create/via_global_ref/store.py b/src/virtualenv/create/via_global_ref/store.py index 0169f827c..4be668921 100644 --- a/src/virtualenv/create/via_global_ref/store.py +++ b/src/virtualenv/create/via_global_ref/store.py @@ -12,7 +12,7 @@ def handle_store_python(meta, interpreter): def is_store_python(interpreter): parts = Path(interpreter.system_executable).parts return ( - len(parts) > 4 + len(parts) > 4 # noqa: PLR2004 and parts[-4] == "Microsoft" and parts[-3] == "WindowsApps" and parts[-2].startswith("PythonSoftwareFoundation.Python.3.") diff --git a/src/virtualenv/create/via_global_ref/venv.py b/src/virtualenv/create/via_global_ref/venv.py index f0ee8446b..cc73a6f27 100644 --- a/src/virtualenv/create/via_global_ref/venv.py +++ b/src/virtualenv/create/via_global_ref/venv.py @@ -5,7 +5,7 @@ from virtualenv.create.via_global_ref.store import handle_store_python from virtualenv.discovery.py_info import PythonInfo -from virtualenv.util.error import ProcessCallFailed +from virtualenv.util.error import ProcessCallFailedError from virtualenv.util.path import ensure_dir from virtualenv.util.subprocess import run_cmd @@ -14,7 +14,7 @@ class Venv(ViaGlobalRefApi): - def __init__(self, options, interpreter): + def __init__(self, options, interpreter) -> None: self.describe = options.describe super().__init__(options, interpreter) current = PythonInfo.current() @@ -48,7 +48,7 @@ def executables_for_win_pypy_less_v37(self): PyPy <= 3.6 (v7.3.3) for Windows contains only pypy3.exe and pypy3w.exe Venv does not handle non-existing exe sources, e.g. python.exe, so this patch does it. - """ + """ # noqa: D205 creator = self.describe if isinstance(creator, Pypy3Windows) and creator.less_v37: for exe in creator.executables(self.interpreter): @@ -70,7 +70,7 @@ def create_via_sub_process(self): logging.info("using host built-in venv to create via %s", " ".join(cmd)) code, out, err = run_cmd(cmd) if code != 0: - raise ProcessCallFailed(code, out, err, cmd) + raise ProcessCallFailedError(code, out, err, cmd) def get_host_create_cmd(self): cmd = [self.interpreter.system_executable, "-m", "venv", "--without-pip"] diff --git a/src/virtualenv/discovery/builtin.py b/src/virtualenv/discovery/builtin.py index 58a912ddd..92d96a91b 100644 --- a/src/virtualenv/discovery/builtin.py +++ b/src/virtualenv/discovery/builtin.py @@ -12,7 +12,7 @@ class Builtin(Discover): - def __init__(self, options): + def __init__(self, options) -> None: super().__init__(options) self.python_spec = options.python if options.python else [sys.executable] self.app_data = options.app_data @@ -48,7 +48,7 @@ def run(self): return result return None - def __repr__(self): + def __repr__(self) -> str: spec = self.python_spec[0] if len(self.python_spec) == 1 else self.python_spec return f"{self.__class__.__name__} discover of python_spec={spec!r}" @@ -67,9 +67,10 @@ def get_interpreter(key, try_first_with, app_data=None, env=None): logging.debug("accepted %s", interpreter) return interpreter proposed_paths.add(key) + return None -def propose_interpreters(spec, try_first_with, app_data, env=None): +def propose_interpreters(spec, try_first_with, app_data, env=None): # noqa: C901, PLR0912 # 0. try with first env = os.environ if env is None else env for py_exe in try_first_with: @@ -126,20 +127,16 @@ def get_paths(env): path = os.confstr("CS_PATH") except (AttributeError, ValueError): path = os.defpath - if not path: - paths = [] - else: - paths = [p for p in path.split(os.pathsep) if os.path.exists(p)] - return paths + return [] if not path else [p for p in path.split(os.pathsep) if os.path.exists(p)] class LazyPathDump: - def __init__(self, pos, path, env): + def __init__(self, pos, path, env) -> None: self.pos = pos self.path = path self.env = env - def __repr__(self): + def __repr__(self) -> str: content = f"discover PATH[{self.pos}]={self.path}" if self.env.get("_VIRTUALENV_DEBUG"): # this is the over the board debug content += " with =>" @@ -175,7 +172,7 @@ def possible_specs(spec): class PathPythonInfo(PythonInfo): - """python info from path""" + """python info from path.""" __all__ = [ diff --git a/src/virtualenv/discovery/cached_py_info.py b/src/virtualenv/discovery/cached_py_info.py index e8cc34cf0..19e938f6c 100644 --- a/src/virtualenv/discovery/cached_py_info.py +++ b/src/virtualenv/discovery/cached_py_info.py @@ -3,7 +3,7 @@ We acquire the python information by running an interrogation script via subprocess trigger. This operation is not cheap, especially not on Windows. To not have to pay this hefty cost every time we apply multiple levels of caching. -""" +""" # noqa: D205 from __future__ import annotations @@ -25,19 +25,18 @@ _CACHE[Path(sys.executable)] = PythonInfo() -def from_exe(cls, app_data, exe, env=None, raise_on_error=True, ignore_cache=False): +def from_exe(cls, app_data, exe, env=None, raise_on_error=True, ignore_cache=False): # noqa: FBT002, PLR0913 env = os.environ if env is None else env result = _get_from_cache(cls, app_data, exe, env, ignore_cache=ignore_cache) if isinstance(result, Exception): if raise_on_error: raise result - else: - logging.info("%s", result) + logging.info("%s", result) result = None return result -def _get_from_cache(cls, app_data, exe, env, ignore_cache=True): +def _get_from_cache(cls, app_data, exe, env, ignore_cache=True): # noqa: FBT002 # note here we cannot resolve symlinks, as the symlink may trigger different prefix information if there's a # pyenv.cfg somewhere alongside on python3.5+ exe_path = Path(exe) @@ -76,7 +75,7 @@ def _get_via_file_cache(cls, app_data, path, exe, env): if py_info is None: # if not loaded run and save failure, py_info = _run_subprocess(cls, exe, app_data, env) if failure is None: - data = {"st_mtime": path_modified, "path": path_text, "content": py_info._to_dict()} + data = {"st_mtime": path_modified, "path": path_text, "content": py_info._to_dict()} # noqa: SLF001 py_info_store.write(data) else: py_info = failure @@ -87,7 +86,9 @@ def _get_via_file_cache(cls, app_data, path, exe, env): def gen_cookie(): - return "".join(random.choice("".join((ascii_lowercase, ascii_uppercase, digits))) for _ in range(COOKIE_LENGTH)) + return "".join( + random.choice(f"{ascii_lowercase}{ascii_uppercase}{digits}") for _ in range(COOKIE_LENGTH) # noqa: S311 + ) def _run_subprocess(cls, exe, app_data, env): @@ -110,7 +111,7 @@ def _run_subprocess(cls, exe, app_data, env): logging.debug("get interpreter info via cmd: %s", LogCmd(cmd)) try: process = Popen( - cmd, + cmd, # noqa: S603 universal_newlines=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, @@ -153,11 +154,11 @@ def _run_subprocess(cls, exe, app_data, env): class LogCmd: - def __init__(self, cmd, env=None): + def __init__(self, cmd, env=None) -> None: self.cmd = cmd self.env = env - def __repr__(self): + def __repr__(self) -> str: cmd_repr = " ".join(quote(str(c)) for c in self.cmd) if self.env is not None: cmd_repr = f"{cmd_repr} env of {self.env!r}" diff --git a/src/virtualenv/discovery/discover.py b/src/virtualenv/discovery/discover.py index 28e07dd4c..b74ee6c01 100644 --- a/src/virtualenv/discovery/discover.py +++ b/src/virtualenv/discovery/discover.py @@ -4,18 +4,20 @@ class Discover(metaclass=ABCMeta): - """Discover and provide the requested Python interpreter""" + """Discover and provide the requested Python interpreter.""" @classmethod - def add_parser_arguments(cls, parser): # noqa: U100 - """Add CLI arguments for this discovery mechanisms. + def add_parser_arguments(cls, parser): + """ + Add CLI arguments for this discovery mechanisms. :param parser: the CLI parser """ raise NotImplementedError - def __init__(self, options): - """Create a new discovery mechanism. + def __init__(self, options) -> None: + """ + Create a new discovery mechanism. :param options: the parsed options as defined within :meth:`add_parser_arguments` """ @@ -25,7 +27,8 @@ def __init__(self, options): @abstractmethod def run(self): - """Discovers an interpreter. + """ + Discovers an interpreter. :return: the interpreter ready to use for virtual environment creation """ @@ -33,9 +36,7 @@ def run(self): @property def interpreter(self): - """ - :return: the interpreter as returned by :meth:`run`, cached - """ + """:return: the interpreter as returned by :meth:`run`, cached""" if self._has_run is False: self._interpreter = self.run() self._has_run = True diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index a28c5188f..85f563761 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -1,5 +1,5 @@ """ -The PythonInfo contains information about a concrete instance of a Python interpreter +The PythonInfo contains information about a concrete instance of a Python interpreter. Note: this file is also used to query target interpreters, so can only use standard library methods """ @@ -21,7 +21,7 @@ def _get_path_extensions(): - return list(OrderedDict.fromkeys([""] + os.environ.get("PATHEXT", "").lower().split(os.pathsep))) + return list(OrderedDict.fromkeys(["", *os.environ.get("PATHEXT", "").lower().split(os.pathsep)])) EXTENSIONS = _get_path_extensions() @@ -29,9 +29,9 @@ def _get_path_extensions(): class PythonInfo: - """Contains information for a Python interpreter""" + """Contains information for a Python interpreter.""" - def __init__(self): + def __init__(self) -> None: # noqa: PLR0915 def abs_path(v): return None if v is None else os.path.abspath(v) # unroll relative elements from path (e.g. ..) @@ -128,13 +128,13 @@ def abs_path(v): self._creators = None def _fast_get_system_executable(self): - """Try to get the system executable by just looking at properties""" + """Try to get the system executable by just looking at properties.""" if self.real_prefix or ( self.base_prefix is not None and self.base_prefix != self.prefix ): # if this is a virtual environment if self.real_prefix is None: base_executable = getattr(sys, "_base_executable", None) # some platforms may set this to help us - if base_executable is not None: # use the saved system executable if present + if base_executable is not None: # noqa: SIM102 # use the saved system executable if present if sys.executable != base_executable: # we know we're in a virtual environment, cannot be us if os.path.exists(base_executable): return base_executable @@ -182,7 +182,7 @@ def _distutils_install(): d = dist.Distribution({"script_args": "--no-user-cfg"}) # conf files not parsed so they do not hijack paths if hasattr(sys, "_framework"): - sys._framework = None # disable macOS static paths for framework + sys._framework = None # disable macOS static paths for framework # noqa: SLF001 with warnings.catch_warnings(): # disable warning for PEP-632 warnings.simplefilter("ignore") @@ -190,8 +190,7 @@ def _distutils_install(): i.prefix = os.sep # paths generated are relative to prefix that contains the path sep, this makes it relative i.finalize_options() - result = {key: (getattr(i, f"install_{key}")[1:]).lstrip(os.sep) for key in SCHEME_KEYS} - return result + return {key: (getattr(i, f"install_{key}")[1:]).lstrip(os.sep) for key in SCHEME_KEYS} @property def version_str(self): @@ -224,7 +223,7 @@ def sysconfig_path(self, key, config_var=None, sep=os.sep): config_var = base return pattern.format(**config_var).replace("/", sep) - def creators(self, refresh=False): + def creators(self, refresh=False): # noqa: FBT002 if self._creators is None or refresh is True: from virtualenv.run.plugin.creators import CreatorSelector @@ -256,17 +255,16 @@ def system_exec_prefix(self): return self.real_prefix or self.base_exec_prefix or self.exec_prefix def __unicode__(self): - content = repr(self) - return content + return repr(self) - def __repr__(self): + def __repr__(self) -> str: return "{}({!r})".format( self.__class__.__name__, {k: v for k, v in self.__dict__.items() if not k.startswith("_")}, ) - def __str__(self): - content = "{}({})".format( + def __str__(self) -> str: + return "{}({})".format( self.__class__.__name__, ", ".join( f"{k}={v}" @@ -295,7 +293,6 @@ def __str__(self): if k is not None ), ) - return content @property def spec(self): @@ -309,8 +306,8 @@ def clear_cache(cls, app_data): clear(app_data) cls._cache_exe_discovery.clear() - def satisfies(self, spec, impl_must_match): - """check if a given specification can be satisfied by the this python interpreter instance""" + def satisfies(self, spec, impl_must_match): # noqa: C901 + """Check if a given specification can be satisfied by the this python interpreter instance.""" if spec.path: if self.executable == os.path.abspath(spec.path): return True # if the path is a our own executable path we're done @@ -325,9 +322,12 @@ def satisfies(self, spec, impl_must_match): if basename != spec_path: return False - if impl_must_match: - if spec.implementation is not None and spec.implementation.lower() != self.implementation.lower(): - return False + if ( + impl_must_match + and spec.implementation is not None + and spec.implementation.lower() != self.implementation.lower() + ): + return False if spec.architecture is not None and spec.architecture != self.architecture: return False @@ -345,7 +345,7 @@ def current(cls, app_data=None): """ This locates the current host interpreter information. This might be different than what we run into in case the host python has been upgraded from underneath us. - """ + """ # noqa: D205 if cls._current is None: cls._current = cls.from_exe(sys.executable, app_data, raise_on_error=True, resolve_to_host=False) return cls._current @@ -355,7 +355,7 @@ def current_system(cls, app_data=None): """ This locates the current host interpreter information. This might be different than what we run into in case the host python has been upgraded from underneath us. - """ + """ # noqa: D205 if cls._current_system is None: cls._current_system = cls.from_exe(sys.executable, app_data, raise_on_error=True, resolve_to_host=True) return cls._current_system @@ -366,25 +366,33 @@ def _to_json(self): def _to_dict(self): data = {var: (getattr(self, var) if var not in ("_creators",) else None) for var in vars(self)} - # noinspection PyProtectedMember + data["version_info"] = data["version_info"]._asdict() # namedtuple to dictionary return data @classmethod - def from_exe(cls, exe, app_data=None, raise_on_error=True, ignore_cache=False, resolve_to_host=True, env=None): - """Given a path to an executable get the python information""" + def from_exe( # noqa: PLR0913 + cls, + exe, + app_data=None, + raise_on_error=True, # noqa: FBT002 + ignore_cache=False, # noqa: FBT002 + resolve_to_host=True, # noqa: FBT002 + env=None, + ): + """Given a path to an executable get the python information.""" # this method is not used by itself, so here and called functions can import stuff locally from virtualenv.discovery.cached_py_info import from_exe env = os.environ if env is None else env proposed = from_exe(cls, app_data, exe, env=env, raise_on_error=raise_on_error, ignore_cache=ignore_cache) - # noinspection PyProtectedMember + if isinstance(proposed, PythonInfo) and resolve_to_host: try: - proposed = proposed._resolve_to_system(app_data, proposed) - except Exception as exception: + proposed = proposed._resolve_to_system(app_data, proposed) # noqa: SLF001 + except Exception as exception: # noqa: BLE001 if raise_on_error: - raise exception + raise logging.info("ignore %s due cannot resolve system due to %r", proposed.original_executable, exception) proposed = None return proposed @@ -417,7 +425,8 @@ def _resolve_to_system(cls, app_data, target): for at, (p, t) in enumerate(prefixes.items(), start=1): logging.error("%d: prefix=%s, info=%r", at, p, t) logging.error("%d: prefix=%s, info=%r", len(prefixes) + 1, prefix, target) - raise RuntimeError("prefixes are causing a circle {}".format("|".join(prefixes.keys()))) + msg = "prefixes are causing a circle {}".format("|".join(prefixes.keys())) + raise RuntimeError(msg) prefixes[prefix] = target target = target.discover_exe(app_data, prefix=prefix, exact=False) if target.executable != target.system_executable: @@ -427,7 +436,7 @@ def _resolve_to_system(cls, app_data, target): _cache_exe_discovery = {} - def discover_exe(self, app_data, prefix, exact=True, env=None): + def discover_exe(self, app_data, prefix, exact=True, env=None): # noqa: FBT002 key = prefix, exact if key in self._cache_exe_discovery and prefix: logging.debug("discover exe from cache %s - exact %s: %r", prefix, exact, self._cache_exe_discovery[key]) @@ -453,7 +462,7 @@ def discover_exe(self, app_data, prefix, exact=True, env=None): msg = "failed to detect {} in {}".format("|".join(possible_names), os.pathsep.join(possible_folders)) raise RuntimeError(msg) - def _check_exe(self, app_data, folder, name, exact, discovered, env): + def _check_exe(self, app_data, folder, name, exact, discovered, env): # noqa: PLR0913 exe_path = os.path.join(folder, name) if not os.path.exists(exe_path): return None @@ -491,12 +500,10 @@ def sort_by(info): info.version_info.releaselevel == target.version_info.releaselevel, info.version_info.serial == target.version_info.serial, ] - priority = sum((1 << pos if match else 0) for pos, match in enumerate(reversed(matches))) - return priority + return sum((1 << pos if match else 0) for pos, match in enumerate(reversed(matches))) sorted_discovered = sorted(discovered, key=sort_by, reverse=True) # sort by priority in decreasing order - most_likely = sorted_discovered[0] - return most_likely + return sorted_discovered[0] def _find_possible_folders(self, inside_folder): candidate_folder = OrderedDict() @@ -505,7 +512,7 @@ def _find_possible_folders(self, inside_folder): executables[self.executable] = None executables[os.path.realpath(self.original_executable)] = None executables[self.original_executable] = None - for exe in executables.keys(): + for exe in executables: base = os.path.dirname(exe) # following path pattern of the current if base.startswith(self.prefix): @@ -514,7 +521,7 @@ def _find_possible_folders(self, inside_folder): # or at root level candidate_folder[inside_folder] = None - return [i for i in candidate_folder.keys() if os.path.exists(i)] + return [i for i in candidate_folder if os.path.exists(i)] def _find_possible_exe_names(self): name_candidate = OrderedDict() @@ -551,7 +558,7 @@ def _possible_base(self): if __name__ == "__main__": # dump a JSON representation of the current python - # noinspection PyProtectedMember + argv = sys.argv[1:] if len(argv) >= 1: @@ -568,5 +575,5 @@ def _possible_base(self): sys.argv = sys.argv[:1] + argv - info = PythonInfo()._to_json() + info = PythonInfo()._to_json() # noqa: SLF001 sys.stdout.write("".join((start_cookie[::-1], info, end_cookie[::-1]))) diff --git a/src/virtualenv/discovery/py_spec.py b/src/virtualenv/discovery/py_spec.py index 04ff645e3..25071b0f6 100644 --- a/src/virtualenv/discovery/py_spec.py +++ b/src/virtualenv/discovery/py_spec.py @@ -1,7 +1,8 @@ -"""A Python specification is an abstract requirement definition of an interpreter""" +"""A Python specification is an abstract requirement definition of an interpreter.""" from __future__ import annotations +import contextlib import os import re from collections import OrderedDict @@ -12,9 +13,9 @@ class PythonSpec: - """Contains specification about a Python Interpreter""" + """Contains specification about a Python Interpreter.""" - def __init__(self, str_spec, implementation, major, minor, micro, architecture, path): + def __init__(self, str_spec, implementation, major, minor, micro, architecture, path) -> None: # noqa: PLR0913 self.str_spec = str_spec self.implementation = implementation self.major = major @@ -24,7 +25,7 @@ def __init__(self, str_spec, implementation, major, minor, micro, architecture, self.path = path @classmethod - def from_string_spec(cls, string_spec): + def from_string_spec(cls, string_spec): # noqa: C901, PLR0912 impl, major, minor, micro, arch, path = None, None, None, None, None, None if os.path.isabs(string_spec): path = string_spec @@ -41,16 +42,16 @@ def _int_or_none(val): version = groups["version"] if version is not None: versions = tuple(int(i) for i in version.split(".") if i) - if len(versions) > 3: - raise ValueError - if len(versions) == 3: + if len(versions) > 3: # noqa: PLR2004 + raise ValueError # noqa: TRY301 + if len(versions) == 3: # noqa: PLR2004 major, minor, micro = versions - elif len(versions) == 2: + elif len(versions) == 2: # noqa: PLR2004 major, minor = versions elif len(versions) == 1: version_data = versions[0] major = int(str(version_data)[0]) # first digit major - if version_data > 9: + if version_data > 9: # noqa: PLR2004 minor = int(str(version_data)[1:]) ok = True except ValueError: @@ -78,10 +79,9 @@ def generate_names(self): impls[self.implementation.upper()] = False impls["python"] = True # finally consider python as alias, implementation must match now version = self.major, self.minor, self.micro - try: + with contextlib.suppress(ValueError): version = version[: version.index(None)] - except ValueError: - pass + for impl, match in impls.items(): for at in range(len(version), -1, -1): cur_ver = version[0:at] @@ -93,7 +93,7 @@ def is_abs(self): return self.path is not None and os.path.isabs(self.path) def satisfies(self, spec): - """called when there's a candidate metadata spec to see if compatible - e.g. PEP-514 on Windows""" + """Called when there's a candidate metadata spec to see if compatible - e.g. PEP-514 on Windows.""" if spec.is_abs and self.is_abs and self.path != spec.path: return False if spec.implementation is not None and spec.implementation.lower() != self.implementation.lower(): @@ -106,7 +106,7 @@ def satisfies(self, spec): return False return True - def __repr__(self): + def __repr__(self) -> str: name = type(self).__name__ params = "implementation", "major", "minor", "micro", "architecture", "path" return f"{name}({', '.join(f'{k}={getattr(self, k)}' for k in params if getattr(self, k) is not None)})" diff --git a/src/virtualenv/discovery/windows/__init__.py b/src/virtualenv/discovery/windows/__init__.py index f2ceb4b28..9efd5b6ab 100644 --- a/src/virtualenv/discovery/windows/__init__.py +++ b/src/virtualenv/discovery/windows/__init__.py @@ -1,7 +1,8 @@ from __future__ import annotations -from ..py_info import PythonInfo -from ..py_spec import PythonSpec +from virtualenv.discovery.py_info import PythonInfo +from virtualenv.discovery.py_spec import PythonSpec + from .pep514 import discover_pythons # Map of well-known organizations (as per PEP 514 Company Windows Registry key part) versus Python implementation @@ -12,7 +13,7 @@ class Pep514PythonInfo(PythonInfo): - """A Python information acquired from PEP-514""" + """A Python information acquired from PEP-514.""" def propose_interpreters(spec, cache_dir, env): @@ -22,7 +23,7 @@ def propose_interpreters(spec, cache_dir, env): # and prefer PythonCore over conda pythons (as virtualenv is mostly used by non conda tools) existing = list(discover_pythons()) existing.sort( - key=lambda i: tuple(-1 if j is None else j for j in i[1:4]) + (1 if i[0] == "PythonCore" else 0,), + key=lambda i: (*tuple(-1 if j is None else j for j in i[1:4]), 1 if i[0] == "PythonCore" else 0), reverse=True, ) @@ -36,10 +37,8 @@ def propose_interpreters(spec, cache_dir, env): registry_spec = PythonSpec(None, implementation, major, minor, None, arch, exe) if skip_pre_filter or registry_spec.satisfies(spec): interpreter = Pep514PythonInfo.from_exe(exe, cache_dir, env=env, raise_on_error=False) - if interpreter is not None: - # Final filtering/matching using interpreter metadata - if interpreter.satisfies(spec, impl_must_match=True): - yield interpreter + if interpreter is not None and interpreter.satisfies(spec, impl_must_match=True): + yield interpreter # Final filtering/matching using interpreter metadata __all__ = [ diff --git a/src/virtualenv/discovery/windows/pep514.py b/src/virtualenv/discovery/windows/pep514.py index ccf3ec1b0..ff1d7c169 100644 --- a/src/virtualenv/discovery/windows/pep514.py +++ b/src/virtualenv/discovery/windows/pep514.py @@ -1,4 +1,4 @@ -"""Implement https://www.python.org/dev/peps/pep-0514/ to discover interpreters - Windows only""" +"""Implement https://www.python.org/dev/peps/pep-0514/ to discover interpreters - Windows only.""" from __future__ import annotations @@ -66,26 +66,27 @@ def process_tag(hive_name, company, company_key, tag, default_arch): if exe_data is not None: exe, args = exe_data return company, major, minor, arch, exe, args + return None + return None + return None def load_exe(hive_name, company, company_key, tag): key_path = f"{hive_name}/{company}/{tag}" try: - with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key: - with ip_key: - exe = get_value(ip_key, "ExecutablePath") - if exe is None: - ip = get_value(ip_key, None) - if ip is None: - msg(key_path, "no ExecutablePath or default for it") - - else: - exe = os.path.join(ip, "python.exe") - if exe is not None and os.path.exists(exe): - args = get_value(ip_key, "ExecutableArguments") - return exe, args + with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key: + exe = get_value(ip_key, "ExecutablePath") + if exe is None: + ip = get_value(ip_key, None) + if ip is None: + msg(key_path, "no ExecutablePath or default for it") + else: - msg(key_path, f"could not load exe with value {exe}") + exe = os.path.join(ip, "python.exe") + if exe is not None and os.path.exists(exe): + args = get_value(ip_key, "ExecutableArguments") + return exe, args + msg(key_path, f"could not load exe with value {exe}") except OSError: msg(f"{key_path}/InstallPath", "missing") return None @@ -109,7 +110,7 @@ def parse_arch(arch_str): return int(next(iter(match.groups()))) error = f"invalid format {arch_str}" else: - error = f"arch is not string: {repr(arch_str)}" + error = f"arch is not string: {arch_str!r}" raise ValueError(error) @@ -146,7 +147,7 @@ def _run(): interpreters = [] for spec in discover_pythons(): interpreters.append(repr(spec)) - print("\n".join(sorted(interpreters))) + print("\n".join(sorted(interpreters))) # noqa: T201 if __name__ == "__main__": diff --git a/src/virtualenv/info.py b/src/virtualenv/info.py index 1562ff2da..e0977768c 100644 --- a/src/virtualenv/info.py +++ b/src/virtualenv/info.py @@ -18,7 +18,7 @@ def fs_is_case_sensitive(): - global _FS_CASE_SENSITIVE + global _FS_CASE_SENSITIVE # noqa: PLW0603 if _FS_CASE_SENSITIVE is None: with tempfile.NamedTemporaryFile(prefix="TmP") as tmp_file: @@ -28,7 +28,7 @@ def fs_is_case_sensitive(): def fs_supports_symlink(): - global _CAN_SYMLINK + global _CAN_SYMLINK # noqa: PLW0603 if _CAN_SYMLINK is None: can = False diff --git a/src/virtualenv/report.py b/src/virtualenv/report.py index be17216b6..db1be9934 100644 --- a/src/virtualenv/report.py +++ b/src/virtualenv/report.py @@ -16,7 +16,7 @@ LOGGER = logging.getLogger() -def setup_report(verbosity, show_pid=False): +def setup_report(verbosity, show_pid=False): # noqa: FBT002 _clean_handlers(LOGGER) if verbosity > MAX_LEVEL: verbosity = MAX_LEVEL # pragma: no cover diff --git a/src/virtualenv/run/__init__.py b/src/virtualenv/run/__init__.py index ea2a40cb7..ed0d6d0cd 100644 --- a/src/virtualenv/run/__init__.py +++ b/src/virtualenv/run/__init__.py @@ -4,19 +4,20 @@ import os from functools import partial -from ..app_data import make_app_data -from ..config.cli.parser import VirtualEnvConfigParser -from ..report import LEVELS, setup_report -from ..run.session import Session -from ..seed.wheels.periodic_update import manual_upgrade -from ..version import __version__ +from virtualenv.app_data import make_app_data +from virtualenv.config.cli.parser import VirtualEnvConfigParser +from virtualenv.report import LEVELS, setup_report +from virtualenv.run.session import Session +from virtualenv.seed.wheels.periodic_update import manual_upgrade +from virtualenv.version import __version__ + from .plugin.activators import ActivationSelector from .plugin.creators import CreatorSelector from .plugin.discovery import get_discover from .plugin.seeders import SeederSelector -def cli_run(args, options=None, setup_logging=True, env=None): +def cli_run(args, options=None, setup_logging=True, env=None): # noqa: FBT002 """ Create a virtual environment given some command line interface arguments. @@ -33,7 +34,7 @@ def cli_run(args, options=None, setup_logging=True, env=None): return of_session -def session_via_cli(args, options=None, setup_logging=True, env=None): +def session_via_cli(args, options=None, setup_logging=True, env=None): # noqa: FBT002 """ Create a virtualenv session (same as cli_run, but this does not perform the creation). Use this if you just want to query what the virtual environment would look like, but not actually create it. @@ -43,16 +44,22 @@ def session_via_cli(args, options=None, setup_logging=True, env=None): :param setup_logging: ``True`` if setup logging handlers, ``False`` to use handlers already registered :param env: environment variables to use :return: the session object of the creation (its structure for now is experimental and might change on short notice) - """ + """ # noqa: D205 env = os.environ if env is None else env parser, elements = build_parser(args, options, setup_logging, env) options = parser.parse_args(args) creator, seeder, activators = tuple(e.create(options) for e in elements) # create types - of_session = Session(options.verbosity, options.app_data, parser._interpreter, creator, seeder, activators) - return of_session + return Session( + options.verbosity, + options.app_data, + parser._interpreter, # noqa: SLF001 + creator, + seeder, + activators, + ) -def build_parser(args=None, options=None, setup_logging=True, env=None): +def build_parser(args=None, options=None, setup_logging=True, env=None): # noqa: FBT002 parser = VirtualEnvConfigParser(options, os.environ if env is None else env) add_version_flag(parser) parser.add_argument( @@ -67,9 +74,10 @@ def build_parser(args=None, options=None, setup_logging=True, env=None): handle_extra_commands(options) discover = get_discover(parser, args) - parser._interpreter = interpreter = discover.interpreter + parser._interpreter = interpreter = discover.interpreter # noqa: SLF001 if interpreter is None: - raise RuntimeError(f"failed to find interpreter for {discover}") + msg = f"failed to find interpreter for {discover}" + raise RuntimeError(msg) elements = [ CreatorSelector(interpreter, parser), SeederSelector(interpreter, parser), @@ -83,7 +91,7 @@ def build_parser(args=None, options=None, setup_logging=True, env=None): def build_parser_only(args=None): - """Used to provide a parser for the doc generation""" + """Used to provide a parser for the doc generation.""" return build_parser(args)[0] @@ -136,7 +144,7 @@ def add_version_flag(parser): def _do_report_setup(parser, args, setup_logging): - level_map = ", ".join(f"{logging.getLevelName(l)}={c}" for c, l in sorted(LEVELS.items())) + level_map = ", ".join(f"{logging.getLevelName(line)}={c}" for c, line in sorted(LEVELS.items())) msg = "verbosity = verbose - quiet, default {}, mapping => {}" verbosity_group = parser.add_argument_group( title="verbosity", diff --git a/src/virtualenv/run/plugin/activators.py b/src/virtualenv/run/plugin/activators.py index 0d4fd8f77..a0e866948 100644 --- a/src/virtualenv/run/plugin/activators.py +++ b/src/virtualenv/run/plugin/activators.py @@ -7,7 +7,7 @@ class ActivationSelector(ComponentBuilder): - def __init__(self, interpreter, parser): + def __init__(self, interpreter, parser) -> None: self.default = None possible = OrderedDict( (k, v) for k, v in self.options("virtualenv.activate").items() if v.supports(interpreter) @@ -31,7 +31,8 @@ def _extract_activators(self, entered_str): elements = [e.strip() for e in entered_str.split(",") if e.strip()] missing = [e for e in elements if e not in self.possible] if missing: - raise ArgumentTypeError(f"the following activators are not available {','.join(missing)}") + msg = f"the following activators are not available {','.join(missing)}" + raise ArgumentTypeError(msg) return elements def handle_selected_arg_parse(self, options): diff --git a/src/virtualenv/run/plugin/base.py b/src/virtualenv/run/plugin/base.py index ddb236998..71ce5c4f4 100644 --- a/src/virtualenv/run/plugin/base.py +++ b/src/virtualenv/run/plugin/base.py @@ -21,18 +21,17 @@ class PluginLoader: def entry_points_for(cls, key): if sys.version_info >= (3, 10) or importlib_metadata_version >= (3, 6): return OrderedDict((e.name, e.load()) for e in cls.entry_points().select(group=key)) - else: - return OrderedDict((e.name, e.load()) for e in cls.entry_points().get(key, {})) + return OrderedDict((e.name, e.load()) for e in cls.entry_points().get(key, {})) @staticmethod def entry_points(): - if PluginLoader._ENTRY_POINTS is None: - PluginLoader._ENTRY_POINTS = entry_points() - return PluginLoader._ENTRY_POINTS + if PluginLoader._ENTRY_POINTS is None: # noqa: SLF001 + PluginLoader._ENTRY_POINTS = entry_points() # noqa: SLF001 + return PluginLoader._ENTRY_POINTS # noqa: SLF001 class ComponentBuilder(PluginLoader): - def __init__(self, interpreter, parser, name, possible): + def __init__(self, interpreter, parser, name, possible) -> None: self.interpreter = interpreter self.name = name self._impl_class = None @@ -46,13 +45,14 @@ def options(cls, key): cls._OPTIONS = cls.entry_points_for(key) return cls._OPTIONS - def add_selector_arg_parse(self, name, choices): # noqa: U100 + def add_selector_arg_parse(self, name, choices): raise NotImplementedError def handle_selected_arg_parse(self, options): selected = getattr(options, self.name) if selected not in self.possible: - raise RuntimeError(f"No implementation for {self.interpreter}") + msg = f"No implementation for {self.interpreter}" + raise RuntimeError(msg) self._impl_class = self.possible[selected] self.populate_selected_argparse(selected, options.app_data) return selected diff --git a/src/virtualenv/run/plugin/creators.py b/src/virtualenv/run/plugin/creators.py index fd47db24e..e5f8d682f 100644 --- a/src/virtualenv/run/plugin/creators.py +++ b/src/virtualenv/run/plugin/creators.py @@ -11,7 +11,7 @@ class CreatorSelector(ComponentBuilder): - def __init__(self, interpreter, parser): + def __init__(self, interpreter, parser) -> None: creators, self.key_to_meta, self.describe, self.builtin_key = self.for_interpreter(interpreter) super().__init__(interpreter, parser, "creator", creators) @@ -21,7 +21,8 @@ def for_interpreter(cls, interpreter): errors = defaultdict(list) for key, creator_class in cls.options("virtualenv.create").items(): if key == "builtin": - raise RuntimeError("builtin creator is a reserved name") + msg = "builtin creator is a reserved name" + raise RuntimeError(msg) meta = creator_class.can_create(interpreter) if meta: if meta.error: @@ -39,8 +40,8 @@ def for_interpreter(cls, interpreter): if errors: rows = [f"{k} for creators {', '.join(i.__name__ for i in v)}" for k, v in errors.items()] raise RuntimeError("\n".join(rows)) - else: - raise RuntimeError(f"No virtualenv implementation for {interpreter}") + msg = f"No virtualenv implementation for {interpreter}" + raise RuntimeError(msg) return CreatorInfo( key_to_class=key_to_class, key_to_meta=key_to_meta, diff --git a/src/virtualenv/run/plugin/discovery.py b/src/virtualenv/run/plugin/discovery.py index caabc95df..c9e456469 100644 --- a/src/virtualenv/run/plugin/discovery.py +++ b/src/virtualenv/run/plugin/discovery.py @@ -4,7 +4,7 @@ class Discovery(PluginLoader): - """Discovery plugins""" + """Discovery plugins.""" def get_discover(parser, args): @@ -27,8 +27,7 @@ def get_discover(parser, args): discover_class = discover_types[options.discovery] discover_class.add_parser_arguments(discovery_parser) options, _ = parser.parse_known_args(args, namespace=options) - discover = discover_class(options) - return discover + return discover_class(options) def _get_default_discovery(discover_types): diff --git a/src/virtualenv/run/plugin/seeders.py b/src/virtualenv/run/plugin/seeders.py index f1cd60547..b1da34c5d 100644 --- a/src/virtualenv/run/plugin/seeders.py +++ b/src/virtualenv/run/plugin/seeders.py @@ -4,7 +4,7 @@ class SeederSelector(ComponentBuilder): - def __init__(self, interpreter, parser): + def __init__(self, interpreter, parser) -> None: possible = self.options("virtualenv.seed") super().__init__(interpreter, parser, "seeder", possible) diff --git a/src/virtualenv/run/session.py b/src/virtualenv/run/session.py index 15ec5974d..9ffd89082 100644 --- a/src/virtualenv/run/session.py +++ b/src/virtualenv/run/session.py @@ -5,9 +5,9 @@ class Session: - """Represents a virtual environment creation session""" + """Represents a virtual environment creation session.""" - def __init__(self, verbosity, app_data, interpreter, creator, seeder, activators): + def __init__(self, verbosity, app_data, interpreter, creator, seeder, activators) -> None: # noqa: PLR0913 self._verbosity = verbosity self._app_data = app_data self._interpreter = interpreter @@ -17,27 +17,27 @@ def __init__(self, verbosity, app_data, interpreter, creator, seeder, activators @property def verbosity(self): - """The verbosity of the run""" + """The verbosity of the run.""" return self._verbosity @property def interpreter(self): - """Create a virtual environment based on this reference interpreter""" + """Create a virtual environment based on this reference interpreter.""" return self._interpreter @property def creator(self): - """The creator used to build the virtual environment (must be compatible with the interpreter)""" + """The creator used to build the virtual environment (must be compatible with the interpreter).""" return self._creator @property def seeder(self): - """The mechanism used to provide the seed packages (pip, setuptools, wheel)""" + """The mechanism used to provide the seed packages (pip, setuptools, wheel).""" return self._seeder @property def activators(self): - """Activators used to generate activations scripts""" + """Activators used to generate activations scripts.""" return self._activators def run(self): @@ -67,7 +67,7 @@ def _activate(self): def __enter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): # noqa: U100 + def __exit__(self, exc_type, exc_val, exc_tb): self._app_data.close() @@ -75,12 +75,12 @@ def __exit__(self, exc_type, exc_val, exc_tb): # noqa: U100 class _Debug: - """lazily populate debug""" + """lazily populate debug.""" - def __init__(self, creator): + def __init__(self, creator) -> None: self.creator = creator - def __repr__(self): + def __repr__(self) -> str: return json.dumps(self.creator.debug, indent=2) diff --git a/src/virtualenv/seed/embed/base_embed.py b/src/virtualenv/seed/embed/base_embed.py index 80a3364af..5ff2c847b 100644 --- a/src/virtualenv/seed/embed/base_embed.py +++ b/src/virtualenv/seed/embed/base_embed.py @@ -3,14 +3,14 @@ from abc import ABCMeta from pathlib import Path -from ..seeder import Seeder -from ..wheels import Version +from virtualenv.seed.seeder import Seeder +from virtualenv.seed.wheels import Version PERIODIC_UPDATE_ON_BY_DEFAULT = True class BaseEmbed(Seeder, metaclass=ABCMeta): - def __init__(self, options): + def __init__(self, options) -> None: super().__init__(options, enabled=options.no_seed is False) self.download = options.download @@ -45,7 +45,7 @@ def distribution_to_versions(self) -> dict[str, str]: } @classmethod - def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: U100 + def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: ARG003 group = parser.add_mutually_exclusive_group() group.add_argument( "--no-download", @@ -72,7 +72,7 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: U100 ) for distribution, default in cls.distributions().items(): if interpreter.version_info[:2] >= (3, 12) and distribution in {"wheel", "setuptools"}: - default = "none" + default = "none" # noqa: PLW2901 parser.add_argument( f"--{distribution}", dest=distribution, diff --git a/src/virtualenv/seed/embed/pip_invoke.py b/src/virtualenv/seed/embed/pip_invoke.py index 032f14d85..2625a01de 100644 --- a/src/virtualenv/seed/embed/pip_invoke.py +++ b/src/virtualenv/seed/embed/pip_invoke.py @@ -6,12 +6,11 @@ from virtualenv.discovery.cached_py_info import LogCmd from virtualenv.seed.embed.base_embed import BaseEmbed - -from ..wheels import Version, get_wheel, pip_wheel_env_run +from virtualenv.seed.wheels import Version, get_wheel, pip_wheel_env_run class PipInvoke(BaseEmbed): - def __init__(self, options): + def __init__(self, options) -> None: super().__init__(options) def run(self, creator): @@ -25,10 +24,11 @@ def run(self, creator): @staticmethod def _execute(cmd, env): logging.debug("pip seed by running: %s", LogCmd(cmd, env)) - process = Popen(cmd, env=env) + process = Popen(cmd, env=env) # noqa: S603 process.communicate() if process.returncode != 0: - raise RuntimeError(f"failed seed with code {process.returncode}") + msg = f"failed seed with code {process.returncode}" + raise RuntimeError(msg) return process @contextmanager @@ -49,7 +49,8 @@ def get_pip_install_cmd(self, exe, for_py_version): env=self.env, ) if wheel is None: - raise RuntimeError(f"could not get wheel for distribution {dist}") + msg = f"could not get wheel for distribution {dist}" + raise RuntimeError(msg) folders.add(str(wheel.path.parent)) cmd.append(Version.as_pip_req(dist, wheel.version)) for folder in sorted(folders): diff --git a/src/virtualenv/seed/embed/via_app_data/pip_install/base.py b/src/virtualenv/seed/embed/via_app_data/pip_install/base.py index 5e87b80af..cc3b73698 100644 --- a/src/virtualenv/seed/embed/via_app_data/pip_install/base.py +++ b/src/virtualenv/seed/embed/via_app_data/pip_install/base.py @@ -16,7 +16,7 @@ class PipInstall(metaclass=ABCMeta): - def __init__(self, wheel, creator, image_folder): + def __init__(self, wheel, creator, image_folder) -> None: self._wheel = wheel self._creator = creator self._image_dir = image_folder @@ -60,7 +60,7 @@ def _shorten_path_if_needed(self, zip_ref): # https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation zip_max_len = max(len(i) for i in zip_ref.namelist()) path_len = zip_max_len + len(to_folder) - if path_len > 260: + if path_len > 260: # noqa: PLR2004 self._image_dir.mkdir(exist_ok=True) # to get a short path must exist from virtualenv.util.path import get_short_path_name @@ -106,7 +106,8 @@ def _dist_info(self): self.__dist_info = filename break else: - raise RuntimeError(f"no .dist-info at {self._image_dir}, has {', '.join(files)}") # pragma: no cover + msg = f"no .dist-info at {self._image_dir}, has {', '.join(files)}" + raise RuntimeError(msg) # pragma: no cover return self.__dist_info @abstractmethod @@ -127,9 +128,8 @@ def _console_scripts(self): if "console_scripts" in parser.sections(): for name, value in parser.items("console_scripts"): match = re.match(r"(.*?)-?\d\.?\d*", name) - if match: - name = match.groups(1)[0] - self._console_entry_points[name] = value + our_name = match.groups(1)[0] if match else name + self._console_entry_points[our_name] = value return self._console_entry_points def _create_console_entry_point(self, name, value, to_folder, version_info): @@ -142,7 +142,7 @@ def _create_console_entry_point(self, name, value, to_folder, version_info): def _uninstall_previous_version(self): dist_name = self._dist_info.stem.split("-")[0] - in_folders = chain.from_iterable([i.iterdir() for i in {self._creator.purelib, self._creator.platlib}]) + in_folders = chain.from_iterable([i.iterdir() for i in (self._creator.purelib, self._creator.platlib)]) paths = (p for p in in_folders if p.stem.split("-")[0] == dist_name and p.suffix == ".dist-info" and p.is_dir()) existing_dist = next(paths, None) if existing_dist is not None: @@ -185,7 +185,7 @@ def has_image(self): class ScriptMakerCustom(ScriptMaker): - def __init__(self, target_dir, version_info, executable, name): + def __init__(self, target_dir, version_info, executable, name) -> None: super().__init__(None, str(target_dir)) self.clobber = True # overwrite self.set_mode = True # ensure they are executable @@ -194,7 +194,7 @@ def __init__(self, target_dir, version_info, executable, name): self.variants = {"", "X", "X.Y"} self._name = name - def _write_script(self, names, shebang, script_bytes, filenames, ext): + def _write_script(self, names, shebang, script_bytes, filenames, ext): # noqa: PLR0913 names.add(f"{self._name}{self.version_info[0]}.{self.version_info[1]}") super()._write_script(names, shebang, script_bytes, filenames, ext) diff --git a/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py b/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py index 7460ffffd..6bc5e51c0 100644 --- a/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py +++ b/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py @@ -16,7 +16,7 @@ def _sync(self, src, dst): def _generate_new_files(self): # create the pyc files, as the build image will be R/O cmd = [str(self._creator.exe), "-m", "compileall", str(self._image_dir)] - process = Popen(cmd, stdout=PIPE, stderr=PIPE) + process = Popen(cmd, stdout=PIPE, stderr=PIPE) # noqa: S603 process.communicate() # the root pyc is shared, so we'll not symlink that - but still add the pyc files to the RECORD for close root_py_cache = self._image_dir / "__pycache__" diff --git a/src/virtualenv/seed/embed/via_app_data/via_app_data.py b/src/virtualenv/seed/embed/via_app_data/via_app_data.py index 0c3625070..7e58bfc6e 100644 --- a/src/virtualenv/seed/embed/via_app_data/via_app_data.py +++ b/src/virtualenv/seed/embed/via_app_data/via_app_data.py @@ -1,4 +1,4 @@ -"""Bootstrap""" +"""Bootstrap.""" from __future__ import annotations @@ -19,7 +19,7 @@ class FromAppData(BaseEmbed): - def __init__(self, options): + def __init__(self, options) -> None: super().__init__(options) self.symlinks = options.symlink_app_data @@ -55,7 +55,7 @@ def _install(name, wheel): if not installer.has_image(): installer.build_image() installer.install(creator.interpreter.version_info) - except Exception: + except Exception: # noqa: BLE001 exceptions[name] = sys.exc_info() threads = [Thread(target=_install, args=(n, w)) for n, w in name_to_whl.items()] @@ -71,7 +71,7 @@ def _install(name, wheel): raise RuntimeError("\n".join(messages)) @contextmanager - def _get_seed_wheels(self, creator): + def _get_seed_wheels(self, creator): # noqa: C901 name_to_whl, lock, fail = {}, Lock(), {} def _get(distribution, version): @@ -124,17 +124,16 @@ def _get(distribution, version): for thread in threads: thread.join() if fail: - raise RuntimeError(f"seed failed due to failing to download wheels {', '.join(fail.keys())}") + msg = f"seed failed due to failing to download wheels {', '.join(fail.keys())}" + raise RuntimeError(msg) yield name_to_whl def installer_class(self, pip_version_tuple): - if self.symlinks and pip_version_tuple: - # symlink support requires pip 19.3+ - if pip_version_tuple >= (19, 3): - return SymlinkPipInstall + if self.symlinks and pip_version_tuple and pip_version_tuple >= (19, 3): # symlink support requires pip 19.3+ + return SymlinkPipInstall return CopyPipInstall - def __repr__(self): + def __repr__(self) -> str: msg = f", via={'symlink' if self.symlinks else 'copy'}, app_data_dir={self.app_data}" base = super().__repr__() return f"{base[:-1]}{msg}{base[-1]}" diff --git a/src/virtualenv/seed/seeder.py b/src/virtualenv/seed/seeder.py index a87c2a3e9..01f943098 100644 --- a/src/virtualenv/seed/seeder.py +++ b/src/virtualenv/seed/seeder.py @@ -6,8 +6,9 @@ class Seeder(metaclass=ABCMeta): """A seeder will install some seed packages into a virtual environment.""" - def __init__(self, options, enabled): + def __init__(self, options, enabled) -> None: """ + Create. :param options: the parsed options as defined within :meth:`add_parser_arguments` :param enabled: a flag weather the seeder is enabled or not @@ -16,7 +17,7 @@ def __init__(self, options, enabled): self.env = options.env @classmethod - def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: U100 + def add_parser_arguments(cls, parser, interpreter, app_data): """ Add CLI arguments for this seed mechanisms. @@ -28,7 +29,8 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: U100 @abstractmethod def run(self, creator): - """Perform the seed operation. + """ + Perform the seed operation. :param creator: the creator (based of :class:`virtualenv.create.creator.Creator`) we used to create this \ virtual environment diff --git a/src/virtualenv/seed/wheels/acquire.py b/src/virtualenv/seed/wheels/acquire.py index aaa0eb0ac..c5ed731d1 100644 --- a/src/virtualenv/seed/wheels/acquire.py +++ b/src/virtualenv/seed/wheels/acquire.py @@ -1,4 +1,4 @@ -"""Bootstrap""" +"""Bootstrap.""" from __future__ import annotations @@ -13,10 +13,17 @@ from .util import Version, Wheel, discover_wheels -def get_wheel(distribution, version, for_py_version, search_dirs, download, app_data, do_periodic_update, env): - """ - Get a wheel with the given distribution-version-for_py_version trio, by using the extra search dir + download - """ +def get_wheel( # noqa: PLR0913 + distribution, + version, + for_py_version, + search_dirs, + download, + app_data, + do_periodic_update, + env, +): + """Get a wheel with the given distribution-version-for_py_version trio, by using the extra search dir + download.""" # not all wheels are compatible with all python versions, so we need to py version qualify it wheel = None @@ -41,7 +48,7 @@ def get_wheel(distribution, version, for_py_version, search_dirs, download, app_ return wheel -def download_wheel(distribution, version_spec, for_py_version, search_dirs, app_data, to_folder, env): +def download_wheel(distribution, version_spec, for_py_version, search_dirs, app_data, to_folder, env): # noqa: PLR0913 to_download = f"{distribution}{version_spec or ''}" logging.debug("download wheel %s %s to %s", to_download, for_py_version, to_folder) cmd = [ @@ -62,7 +69,7 @@ def download_wheel(distribution, version_spec, for_py_version, search_dirs, app_ ] # pip has no interface in python - must be a new sub-process env = pip_wheel_env_run(search_dirs, app_data, env) - process = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE, universal_newlines=True, encoding="utf-8") + process = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE, universal_newlines=True, encoding="utf-8") # noqa: S603 out, err = process.communicate() if process.returncode != 0: kwargs = {"output": out, "stderr": err} @@ -74,10 +81,10 @@ def download_wheel(distribution, version_spec, for_py_version, search_dirs, app_ def _find_downloaded_wheel(distribution, version_spec, for_py_version, to_folder, out): for line in out.splitlines(): - line = line.lstrip() + stripped_line = line.lstrip() for marker in ("Saved ", "File was already downloaded "): - if line.startswith(marker): - return Wheel(Path(line[len(marker) :]).absolute()) + if stripped_line.startswith(marker): + return Wheel(Path(stripped_line[len(marker) :]).absolute()) # if for some reason the output does not match fallback to the latest version with that spec return find_compatible_in_house(distribution, version_spec, for_py_version, to_folder) @@ -85,7 +92,7 @@ def _find_downloaded_wheel(distribution, version_spec, for_py_version, to_folder def find_compatible_in_house(distribution, version_spec, for_py_version, in_folder): wheels = discover_wheels(in_folder, distribution, None, for_py_version) start, end = 0, len(wheels) - if version_spec is not None and version_spec != "": + if version_spec is not None and version_spec: if version_spec.startswith("<"): from_pos, op = 1, lt elif version_spec.startswith("=="): @@ -112,7 +119,8 @@ def pip_wheel_env_run(search_dirs, app_data, env): env=env, ) if wheel is None: - raise RuntimeError("could not find the embedded pip") + msg = "could not find the embedded pip" + raise RuntimeError(msg) env["PYTHONPATH"] = str(wheel.path) return env diff --git a/src/virtualenv/seed/wheels/bundle.py b/src/virtualenv/seed/wheels/bundle.py index e2c609543..d54ebccda 100644 --- a/src/virtualenv/seed/wheels/bundle.py +++ b/src/virtualenv/seed/wheels/bundle.py @@ -1,14 +1,13 @@ from __future__ import annotations -from ..wheels.embed import get_embed_wheel +from virtualenv.seed.wheels.embed import get_embed_wheel + from .periodic_update import periodic_update from .util import Version, Wheel, discover_wheels -def from_bundle(distribution, version, for_py_version, search_dirs, app_data, do_periodic_update, env): - """ - Load the bundled wheel to a cache directory. - """ +def from_bundle(distribution, version, for_py_version, search_dirs, app_data, do_periodic_update, env): # noqa: PLR0913 + """Load the bundled wheel to a cache directory.""" of_version = Version.of_version(version) wheel = load_embed_wheel(app_data, distribution, for_py_version, of_version) @@ -20,11 +19,8 @@ def from_bundle(distribution, version, for_py_version, search_dirs, app_data, do # 3. acquire from extra search dir found_wheel = from_dir(distribution, of_version, for_py_version, search_dirs) - if found_wheel is not None: - if wheel is None: - wheel = found_wheel - elif found_wheel.version_tuple > wheel.version_tuple: - wheel = found_wheel + if found_wheel is not None and (wheel is None or found_wheel.version_tuple > wheel.version_tuple): + wheel = found_wheel return wheel @@ -41,9 +37,7 @@ def load_embed_wheel(app_data, distribution, for_py_version, version): def from_dir(distribution, version, for_py_version, directories): - """ - Load a compatible wheel from a given folder. - """ + """Load a compatible wheel from a given folder.""" for folder in directories: for wheel in discover_wheels(folder, distribution, version, for_py_version): return wheel diff --git a/src/virtualenv/seed/wheels/embed/__init__.py b/src/virtualenv/seed/wheels/embed/__init__.py index ff3f412a5..2c1206e89 100644 --- a/src/virtualenv/seed/wheels/embed/__init__.py +++ b/src/virtualenv/seed/wheels/embed/__init__.py @@ -8,32 +8,32 @@ BUNDLE_SUPPORT = { "3.7": { "pip": "pip-23.1.2-py3-none-any.whl", - "setuptools": "setuptools-67.7.2-py3-none-any.whl", + "setuptools": "setuptools-67.8.0-py3-none-any.whl", "wheel": "wheel-0.40.0-py3-none-any.whl", }, "3.8": { "pip": "pip-23.1.2-py3-none-any.whl", - "setuptools": "setuptools-67.7.2-py3-none-any.whl", + "setuptools": "setuptools-67.8.0-py3-none-any.whl", "wheel": "wheel-0.40.0-py3-none-any.whl", }, "3.9": { "pip": "pip-23.1.2-py3-none-any.whl", - "setuptools": "setuptools-67.7.2-py3-none-any.whl", + "setuptools": "setuptools-67.8.0-py3-none-any.whl", "wheel": "wheel-0.40.0-py3-none-any.whl", }, "3.10": { "pip": "pip-23.1.2-py3-none-any.whl", - "setuptools": "setuptools-67.7.2-py3-none-any.whl", + "setuptools": "setuptools-67.8.0-py3-none-any.whl", "wheel": "wheel-0.40.0-py3-none-any.whl", }, "3.11": { "pip": "pip-23.1.2-py3-none-any.whl", - "setuptools": "setuptools-67.7.2-py3-none-any.whl", + "setuptools": "setuptools-67.8.0-py3-none-any.whl", "wheel": "wheel-0.40.0-py3-none-any.whl", }, "3.12": { "pip": "pip-23.1.2-py3-none-any.whl", - "setuptools": "setuptools-67.7.2-py3-none-any.whl", + "setuptools": "setuptools-67.8.0-py3-none-any.whl", "wheel": "wheel-0.40.0-py3-none-any.whl", }, } diff --git a/src/virtualenv/seed/wheels/embed/setuptools-67.7.2-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/setuptools-67.8.0-py3-none-any.whl similarity index 93% rename from src/virtualenv/seed/wheels/embed/setuptools-67.7.2-py3-none-any.whl rename to src/virtualenv/seed/wheels/embed/setuptools-67.8.0-py3-none-any.whl index 585bdb3fb..46220caf7 100644 Binary files a/src/virtualenv/seed/wheels/embed/setuptools-67.7.2-py3-none-any.whl and b/src/virtualenv/seed/wheels/embed/setuptools-67.8.0-py3-none-any.whl differ diff --git a/src/virtualenv/seed/wheels/periodic_update.py b/src/virtualenv/seed/wheels/periodic_update.py index d1767ce72..911d5e3ba 100644 --- a/src/virtualenv/seed/wheels/periodic_update.py +++ b/src/virtualenv/seed/wheels/periodic_update.py @@ -1,6 +1,4 @@ -""" -Periodically update bundled versions. -""" +"""Periodically update bundled versions.""" from __future__ import annotations @@ -10,7 +8,7 @@ import os import ssl import sys -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from itertools import groupby from pathlib import Path from shutil import copy2 @@ -21,22 +19,30 @@ from urllib.request import urlopen from virtualenv.app_data import AppDataDiskFolder +from virtualenv.seed.wheels.embed import BUNDLE_SUPPORT +from virtualenv.seed.wheels.util import Wheel from virtualenv.util.subprocess import CREATE_NO_WINDOW -from ..wheels.embed import BUNDLE_SUPPORT -from ..wheels.util import Wheel - GRACE_PERIOD_CI = timedelta(hours=1) # prevent version switch in the middle of a CI run GRACE_PERIOD_MINOR = timedelta(days=28) UPDATE_PERIOD = timedelta(days=14) UPDATE_ABORTED_DELAY = timedelta(hours=1) -def periodic_update(distribution, of_version, for_py_version, wheel, search_dirs, app_data, do_periodic_update, env): +def periodic_update( # noqa: PLR0913 + distribution, + of_version, + for_py_version, + wheel, + search_dirs, + app_data, + do_periodic_update, + env, +): if do_periodic_update: handle_auto_update(distribution, for_py_version, wheel, search_dirs, app_data, env) - now = datetime.now() + now = datetime.now(tz=timezone.utc) def _update_wheel(ver): updated_wheel = Wheel(app_data.house / ver.filename) @@ -62,12 +68,12 @@ def _update_wheel(ver): return wheel -def handle_auto_update(distribution, for_py_version, wheel, search_dirs, app_data, env): +def handle_auto_update(distribution, for_py_version, wheel, search_dirs, app_data, env): # noqa: PLR0913 embed_update_log = app_data.embed_update_log(distribution, for_py_version) u_log = UpdateLog.from_dict(embed_update_log.read()) if u_log.needs_update: u_log.periodic = True - u_log.started = datetime.now() + u_log.started = datetime.now(tz=timezone.utc) embed_update_log.write(u_log.to_dict()) trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, periodic=True, env=env) @@ -80,7 +86,7 @@ def add_wheel_to_update_log(wheel, for_py_version, app_data): logging.warning("%s already present in %s", wheel.name, embed_update_log.file) return # we don't need a release date for sources other than "periodic" - version = NewVersion(wheel.name, datetime.now(), None, "download") + version = NewVersion(wheel.name, datetime.now(tz=timezone.utc), None, "download") u_log.versions.append(version) # always write at the end for proper updates embed_update_log.write(u_log.to_dict()) @@ -93,11 +99,11 @@ def dump_datetime(value): def load_datetime(value): - return None if value is None else datetime.strptime(value, DATETIME_FMT) + return None if value is None else datetime.strptime(value, DATETIME_FMT).replace(tzinfo=timezone.utc) class NewVersion: - def __init__(self, filename, found_date, release_date, source): + def __init__(self, filename, found_date, release_date, source) -> None: self.filename = filename self.found_date = found_date self.release_date = release_date @@ -120,18 +126,17 @@ def to_dict(self): "source": self.source, } - def use(self, now, ignore_grace_period_minor=False, ignore_grace_period_ci=False): + def use(self, now, ignore_grace_period_minor=False, ignore_grace_period_ci=False): # noqa: FBT002 if self.source == "manual": return True - elif self.source == "periodic": - if self.found_date < now - GRACE_PERIOD_CI or ignore_grace_period_ci: - if not ignore_grace_period_minor: - compare_from = self.release_date or self.found_date - return now - compare_from >= GRACE_PERIOD_MINOR - return True + if self.source == "periodic" and (self.found_date < now - GRACE_PERIOD_CI or ignore_grace_period_ci): + if not ignore_grace_period_minor: + compare_from = self.release_date or self.found_date + return now - compare_from >= GRACE_PERIOD_MINOR + return True return False - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(filename={self.filename}), found_date={self.found_date}, " f"release_date={self.release_date}, source={self.source})" @@ -151,7 +156,7 @@ def wheel(self): class UpdateLog: - def __init__(self, started, completed, versions, periodic): + def __init__(self, started, completed, versions, periodic) -> None: self.started = started self.completed = completed self.versions = versions @@ -183,19 +188,18 @@ def to_dict(self): @property def needs_update(self): - now = datetime.now() + now = datetime.now(tz=timezone.utc) if self.completed is None: # never completed return self._check_start(now) - else: - if now - self.completed <= UPDATE_PERIOD: - return False - return self._check_start(now) + if now - self.completed <= UPDATE_PERIOD: + return False + return self._check_start(now) def _check_start(self, now): return self.started is None or now - self.started > UPDATE_ABORTED_DELAY -def trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, env, periodic): +def trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, env, periodic): # noqa: PLR0913 wheel_path = None if wheel is None else str(wheel.path) cmd = [ sys.executable, @@ -216,7 +220,7 @@ def trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, e kwargs = {"stdout": pipe, "stderr": pipe} if not debug and sys.platform == "win32": kwargs["creationflags"] = CREATE_NO_WINDOW - process = Popen(cmd, **kwargs) + process = Popen(cmd, **kwargs) # noqa: S603 logging.info( "triggered periodic upgrade of %s%s (for python %s) via background process having PID %d", distribution, @@ -228,7 +232,7 @@ def trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, e process.communicate() # on purpose not called to make it a background process -def do_update(distribution, for_py_version, embed_filename, app_data, search_dirs, periodic): +def do_update(distribution, for_py_version, embed_filename, app_data, search_dirs, periodic): # noqa: PLR0913 versions = None try: versions = _run_do_update(app_data, distribution, embed_filename, for_py_version, periodic, search_dirs) @@ -237,7 +241,14 @@ def do_update(distribution, for_py_version, embed_filename, app_data, search_dir return versions -def _run_do_update(app_data, distribution, embed_filename, for_py_version, periodic, search_dirs): +def _run_do_update( # noqa: C901, PLR0913 + app_data, + distribution, + embed_filename, + for_py_version, + periodic, + search_dirs, +): from virtualenv.seed.wheels import acquire wheel_filename = None if embed_filename is None else Path(embed_filename) @@ -247,7 +258,7 @@ def _run_do_update(app_data, distribution, embed_filename, for_py_version, perio wheelhouse = app_data.house embed_update_log = app_data.embed_update_log(distribution, for_py_version) u_log = UpdateLog.from_dict(embed_update_log.read()) - now = datetime.now() + now = datetime.now(tz=timezone.utc) update_versions, other_versions = [], [] for version in u_log.versions: @@ -270,7 +281,7 @@ def _run_do_update(app_data, distribution, embed_filename, for_py_version, perio copy2(str(wheel_filename), str(wheelhouse)) last, last_version, versions, filenames = None, None, [], set() while last is None or not last.use(now, ignore_grace_period_ci=True): - download_time = datetime.now() + download_time = datetime.now(tz=timezone.utc) dest = acquire.download_wheel( distribution=distribution, version_spec=None if last_version is None else f"<{last_version}", @@ -284,21 +295,20 @@ def _run_do_update(app_data, distribution, embed_filename, for_py_version, perio break release_date = release_date_for_wheel_path(dest.path) last = NewVersion(filename=dest.path.name, release_date=release_date, found_date=download_time, source=source) - logging.info("detected %s in %s", last, datetime.now() - download_time) + logging.info("detected %s in %s", last, datetime.now(tz=timezone.utc) - download_time) versions.append(last) filenames.add(last.filename) last_wheel = last.wheel last_version = last_wheel.version - if embed_version is not None: - if embed_version >= last_wheel.version_tuple: # stop download if we reach the embed version - break + if embed_version is not None and embed_version >= last_wheel.version_tuple: + break # stop download if we reach the embed version u_log.periodic = periodic if not u_log.periodic: u_log.started = now # update other_versions by removing version we just found other_versions = [version for version in other_versions if version.filename not in filenames] u_log.versions = versions + update_versions + other_versions - u_log.completed = datetime.now() + u_log.completed = datetime.now(tz=timezone.utc) embed_update_log.write(u_log.to_dict()) return versions @@ -311,16 +321,16 @@ def release_date_for_wheel_path(dest): if content is not None: try: upload_time = content["releases"][wheel.version][0]["upload_time"] - return datetime.strptime(upload_time, "%Y-%m-%dT%H:%M:%S") - except Exception as exception: - logging.error("could not load release date %s because %r", content, exception) + return datetime.strptime(upload_time, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc) + except Exception as exception: # noqa: BLE001 + logging.error("could not load release date %s because %r", content, exception) # noqa: TRY400 return None def _request_context(): yield None # fallback to non verified HTTPS (the information we request is not sensitive, so fallback) - yield ssl._create_unverified_context() + yield ssl._create_unverified_context() # noqa: S323, SLF001 _PYPI_CACHE = {} @@ -337,13 +347,13 @@ def _pypi_get_distribution_info(distribution): try: for context in _request_context(): try: - with urlopen(url, context=context) as file_handler: + with urlopen(url, context=context) as file_handler: # noqa: S310 content = json.load(file_handler) break except URLError as exception: - logging.error("failed to access %s because %r", url, exception) - except Exception as exception: - logging.error("failed to access %s because %r", url, exception) + logging.error("failed to access %s because %r", url, exception) # noqa: TRY400 + except Exception as exception: # noqa: BLE001 + logging.error("failed to access %s because %r", url, exception) # noqa: TRY400 return content @@ -352,7 +362,7 @@ def manual_upgrade(app_data, env): for for_py_version, distribution_to_package in BUNDLE_SUPPORT.items(): # load extra search dir for the given for_py - for distribution in distribution_to_package.keys(): + for distribution in distribution_to_package: thread = Thread(target=_run_manual_upgrade, args=(app_data, distribution, for_py_version, env)) thread.start() threads.append(thread) @@ -362,7 +372,7 @@ def manual_upgrade(app_data, env): def _run_manual_upgrade(app_data, distribution, for_py_version, env): - start = datetime.now() + start = datetime.now(tz=timezone.utc) from .bundle import from_bundle current = from_bundle( @@ -392,12 +402,13 @@ def _run_manual_upgrade(app_data, distribution, for_py_version, env): args = [ distribution, for_py_version, - datetime.now() - start, + datetime.now(tz=timezone.utc) - start, ] if versions: args.append("\n".join(f"\t{v}" for v in versions)) ver_update = "new entries found:\n%s" if versions else "no new versions found" - logging.warning(f"upgraded %s for python %s in %s {ver_update}", *args) + msg = f"upgraded %s for python %s in %s {ver_update}" + logging.warning(msg, *args) __all__ = [ diff --git a/src/virtualenv/seed/wheels/util.py b/src/virtualenv/seed/wheels/util.py index 5843575d6..cfe537ea5 100644 --- a/src/virtualenv/seed/wheels/util.py +++ b/src/virtualenv/seed/wheels/util.py @@ -5,7 +5,7 @@ class Wheel: - def __init__(self, path): + def __init__(self, path) -> None: # https://www.python.org/dev/peps/pep-0427/#file-name-convention # The wheel filename is {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl self.path = path @@ -13,7 +13,7 @@ def __init__(self, path): @classmethod def from_path(cls, path): - if path is not None and path.suffix == ".whl" and len(path.stem.split("-")) >= 5: + if path is not None and path.suffix == ".whl" and len(path.stem.split("-")) >= 5: # noqa: PLR2004 return cls(path) return None @@ -72,10 +72,10 @@ def support_py(self, py_version): break return True - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({self.path})" - def __str__(self): + def __str__(self) -> str: return str(self.path) @@ -83,10 +83,13 @@ def discover_wheels(from_folder, distribution, version, for_py_version): wheels = [] for filename in from_folder.iterdir(): wheel = Wheel.from_path(filename) - if wheel and wheel.distribution == distribution: - if version is None or wheel.version == version: - if wheel.support_py(for_py_version): - wheels.append(wheel) + if ( + wheel + and wheel.distribution == distribution + and (version is None or wheel.version == version) + and wheel.support_py(for_py_version) + ): + wheels.append(wheel) return sorted(wheels, key=attrgetter("version_tuple", "distribution"), reverse=True) diff --git a/src/virtualenv/util/error.py b/src/virtualenv/util/error.py index 5862226b3..7b23509bd 100644 --- a/src/virtualenv/util/error.py +++ b/src/virtualenv/util/error.py @@ -1,13 +1,13 @@ -"""Errors""" +"""Errors.""" from __future__ import annotations -class ProcessCallFailed(RuntimeError): - """Failed a process call""" +class ProcessCallFailedError(RuntimeError): + """Failed a process call.""" - def __init__(self, code, out, err, cmd): + def __init__(self, code, out, err, cmd) -> None: super().__init__(code, out, err, cmd) self.code = code self.out = out diff --git a/src/virtualenv/util/lock.py b/src/virtualenv/util/lock.py index c0774d154..c15b5f188 100644 --- a/src/virtualenv/util/lock.py +++ b/src/virtualenv/util/lock.py @@ -1,11 +1,11 @@ -"""holds locking functionality that works across processes""" +"""holds locking functionality that works across processes.""" from __future__ import annotations import logging import os from abc import ABCMeta, abstractmethod -from contextlib import contextmanager +from contextlib import contextmanager, suppress from pathlib import Path from threading import Lock, RLock @@ -13,13 +13,12 @@ class _CountedFileLock(FileLock): - def __init__(self, lock_file): + def __init__(self, lock_file) -> None: parent = os.path.dirname(lock_file) if not os.path.isdir(parent): - try: + with suppress(OSError): os.makedirs(parent) - except OSError: - pass + super().__init__(lock_file) self.count = 0 self.thread_safe = RLock() @@ -31,7 +30,7 @@ def acquire(self, timeout=None, poll_interval=0.05): super().acquire(timeout, poll_interval) self.count += 1 - def release(self, force=False): + def release(self, force=False): # noqa: FBT002 with self.thread_safe: if self.count > 0: self.thread_safe.release() @@ -45,11 +44,11 @@ def release(self, force=False): class PathLockBase(metaclass=ABCMeta): - def __init__(self, folder): + def __init__(self, folder) -> None: path = Path(folder) self.path = path.resolve() if path.exists() else path - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({self.path})" def __div__(self, other): @@ -68,7 +67,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): @abstractmethod @contextmanager - def lock_for_key(self, name, no_block=False): + def lock_for_key(self, name, no_block=False): # noqa: FBT002 raise NotImplementedError @abstractmethod @@ -78,7 +77,7 @@ def non_reentrant_lock_for_key(self, name): class ReentrantFileLock(PathLockBase): - def __init__(self, folder): + def __init__(self, folder) -> None: super().__init__(folder) self._lock = None @@ -92,31 +91,29 @@ def _create_lock(self, name=""): @staticmethod def _del_lock(lock): if lock is not None: - with _store_lock: - with lock.thread_safe: - if lock.count == 0: - _lock_store.pop(lock.lock_file, None) + with _store_lock, lock.thread_safe: + if lock.count == 0: + _lock_store.pop(lock.lock_file, None) - def __del__(self): + def __del__(self) -> None: self._del_lock(self._lock) def __enter__(self): self._lock = self._create_lock() self._lock_file(self._lock) - def __exit__(self, exc_type, exc_val, exc_tb): # noqa: U100 + def __exit__(self, exc_type, exc_val, exc_tb): self._release(self._lock) self._del_lock(self._lock) self._lock = None - def _lock_file(self, lock, no_block=False): + def _lock_file(self, lock, no_block=False): # noqa: FBT002 # multiple processes might be trying to get a first lock... so we cannot check if this directory exist without # a lock, but that lock might then become expensive, and it's not clear where that lock should live. # Instead here we just ignore if we fail to create the directory. - try: + with suppress(OSError): os.makedirs(str(self.path)) - except OSError: - pass + try: lock.acquire(0.0001) except Timeout: @@ -131,7 +128,7 @@ def _release(lock): lock.release() @contextmanager - def lock_for_key(self, name, no_block=False): + def lock_for_key(self, name, no_block=False): # noqa: FBT002 lock = self._create_lock(name) try: try: @@ -153,15 +150,15 @@ class NoOpFileLock(PathLockBase): def __enter__(self): raise NotImplementedError - def __exit__(self, exc_type, exc_val, exc_tb): # noqa: U100 + def __exit__(self, exc_type, exc_val, exc_tb): raise NotImplementedError @contextmanager - def lock_for_key(self, name, no_block=False): # noqa: U100 + def lock_for_key(self, name, no_block=False): # noqa: ARG002, FBT002 yield @contextmanager - def non_reentrant_lock_for_key(self, name): # noqa: U100 + def non_reentrant_lock_for_key(self, name): # noqa: ARG002 yield diff --git a/src/virtualenv/util/path/_sync.py b/src/virtualenv/util/path/_sync.py index a81f82b01..c9334aded 100644 --- a/src/virtualenv/util/path/_sync.py +++ b/src/virtualenv/util/path/_sync.py @@ -15,7 +15,8 @@ def ensure_dir(path): def ensure_safe_to_do(src, dest): if src == dest: - raise ValueError(f"source and destination is the same {src}") + msg = f"source and destination is the same {src}" + raise ValueError(msg) if not dest.exists(): return if dest.is_dir() and not dest.is_symlink(): @@ -52,7 +53,7 @@ def copytree(src, dest): def safe_delete(dest): - def onerror(func, path, exc_info): # noqa: U100 + def onerror(func, path, exc_info): # noqa: ARG001 if not os.access(path, os.W_OK): os.chmod(path, S_IWUSR) func(path) @@ -64,12 +65,12 @@ def onerror(func, path, exc_info): # noqa: U100 class _Debug: - def __init__(self, src, dest): + def __init__(self, src, dest) -> None: self.src = src self.dest = dest - def __str__(self): - return f"{'directory ' if self.src.is_dir() else ''}{str(self.src)} to {str(self.dest)}" + def __str__(self) -> str: + return f"{'directory ' if self.src.is_dir() else ''}{self.src!s} to {self.dest!s}" __all__ = [ diff --git a/src/virtualenv/util/path/_win.py b/src/virtualenv/util/path/_win.py index 94eaf6599..e7385519d 100644 --- a/src/virtualenv/util/path/_win.py +++ b/src/virtualenv/util/path/_win.py @@ -2,10 +2,7 @@ def get_short_path_name(long_name): - """ - Gets the short path name of a given long path. - http://stackoverflow.com/a/23598461/200291 - """ + """Gets the short path name of a given long path - http://stackoverflow.com/a/23598461/200291.""" import ctypes from ctypes import wintypes @@ -18,8 +15,7 @@ def get_short_path_name(long_name): needed = _GetShortPathNameW(long_name, output_buf, output_buf_size) if output_buf_size >= needed: return output_buf.value - else: - output_buf_size = needed + output_buf_size = needed __all__ = [ diff --git a/src/virtualenv/util/subprocess/__init__.py b/src/virtualenv/util/subprocess/__init__.py index cb71fa713..03e5370e8 100644 --- a/src/virtualenv/util/subprocess/__init__.py +++ b/src/virtualenv/util/subprocess/__init__.py @@ -8,7 +8,7 @@ def run_cmd(cmd): try: process = subprocess.Popen( - cmd, + cmd, # noqa: S603 universal_newlines=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, @@ -19,7 +19,7 @@ def run_cmd(cmd): code = process.returncode except OSError as error: code, out, err = error.errno, "", error.strerror - if code == 2 and "file" in err: + if code == 2 and "file" in err: # noqa: PLR2004 err = str(error) # FileNotFoundError in Python >= 3.3 return code, out, err diff --git a/src/virtualenv/util/zipapp.py b/src/virtualenv/util/zipapp.py index 5271ebbbf..3049b8400 100644 --- a/src/virtualenv/util/zipapp.py +++ b/src/virtualenv/util/zipapp.py @@ -9,9 +9,8 @@ def read(full_path): sub_file = _get_path_within_zip(full_path) - with zipfile.ZipFile(ROOT, "r") as zip_file: - with zip_file.open(sub_file) as file_handler: - return file_handler.read().decode("utf-8") + with zipfile.ZipFile(ROOT, "r") as zip_file, zip_file.open(sub_file) as file_handler: + return file_handler.read().decode("utf-8") def extract(full_path, dest): diff --git a/tasks/__main__zipapp.py b/tasks/__main__zipapp.py index e48620bb6..a34fc5c33 100644 --- a/tasks/__main__zipapp.py +++ b/tasks/__main__zipapp.py @@ -6,11 +6,11 @@ import zipfile ABS_HERE = os.path.abspath(os.path.dirname(__file__)) -NEW_IMPORT_SYSTEM = sys.version_info[0] == 3 +NEW_IMPORT_SYSTEM = sys.version_info[0] >= 3 # noqa: PLR2004 class VersionPlatformSelect: - def __init__(self): + def __init__(self) -> None: self.archive = ABS_HERE self._zip_file = zipfile.ZipFile(ABS_HERE, "r") self.modules = self._load("modules.json") @@ -32,13 +32,13 @@ def _load(self, of_file): def __enter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): # noqa: U100 + def __exit__(self, exc_type, exc_val, exc_tb): self._zip_file.close() def find_mod(self, fullname): if fullname in self.modules: - result = self.modules[fullname] - return result + return self.modules[fullname] + return None def get_filename(self, fullname): zip_path = self.find_mod(fullname) @@ -62,7 +62,7 @@ def find_distributions(self, context): result = dist_class(file_loader=self.get_data, dist_path=self.distributions[name]) yield result - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(path={ABS_HERE})" def _register_distutils_finder(self): @@ -70,19 +70,18 @@ def _register_distutils_finder(self): return class DistlibFinder: - def __init__(self, path, loader): + def __init__(self, path, loader) -> None: self.path = path self.loader = loader def find(self, name): class Resource: - def __init__(self, content): + def __init__(self, content) -> None: self.bytes = content full_path = os.path.join(self.path, name) return Resource(self.loader.get_data(full_path)) - # noinspection PyPackageRequirements from distlib.resources import register_finder register_finder(self, lambda module: DistlibFinder(os.path.dirname(module.__file__), self)) @@ -92,17 +91,15 @@ def __init__(self, content): def versioned_distribution_class(): - global _VER_DISTRIBUTION_CLASS + global _VER_DISTRIBUTION_CLASS # noqa: PLW0603 if _VER_DISTRIBUTION_CLASS is None: if sys.version_info >= (3, 8): - # noinspection PyCompatibility from importlib.metadata import Distribution else: - # noinspection PyUnresolvedReferences from importlib_metadata import Distribution class VersionedDistribution(Distribution): - def __init__(self, file_loader, dist_path): + def __init__(self, file_loader, dist_path) -> None: self.file_loader = file_loader self.dist_path = dist_path @@ -117,27 +114,24 @@ def locate_file(self, path): if NEW_IMPORT_SYSTEM: - # noinspection PyCompatibility - # noinspection PyCompatibility 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: U100 + def find_spec(self, fullname, path, target=None): # noqa: ARG002 zip_path = self.find_mod(fullname) if zip_path is not None: - spec = spec_from_file_location(name=fullname, loader=self) - return spec + return spec_from_file_location(name=fullname, loader=self) + return None - def module_repr(self, module): # noqa: U100 + def module_repr(self, module): raise NotImplementedError else: - # noinspection PyDeprecation from imp import new_module class VersionedFindLoad(VersionPlatformSelect): - def find_module(self, fullname, path=None): # noqa: U100 + def find_module(self, fullname, path=None): # noqa: ARG002 return self if self.find_mod(fullname) else None def load_module(self, fullname): @@ -152,14 +146,14 @@ def load_module(self, fullname): mod.__package__ = fullname else: mod.__package__ = fullname.rpartition(".")[0] - exec(code, mod.__dict__) + exec(code, mod.__dict__) # noqa: S102 return mod def run(): with VersionedFindLoad() as finder: sys.meta_path.insert(0, finder) - finder._register_distutils_finder() + finder._register_distutils_finder() # noqa: SLF001 from virtualenv.__main__ import run as run_virtualenv run_virtualenv() diff --git a/tasks/make_zipapp.py b/tasks/make_zipapp.py index 5303f41a4..40f435165 100644 --- a/tasks/make_zipapp.py +++ b/tasks/make_zipapp.py @@ -1,4 +1,4 @@ -"""https://docs.python.org/3/library/zipapp.html""" +"""https://docs.python.org/3/library/zipapp.html.""" from __future__ import annotations import argparse @@ -48,10 +48,10 @@ 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}") + print(f"zipapp created at {dest}") # noqa: T201 -def write_packages_to_zipapp(base, dist, modules, packages, zip_app): +def write_packages_to_zipapp(base, dist, modules, packages, zip_app): # noqa: C901 has = set() for name, p_w_v in packages.items(): for platform, w_v in p_w_v.items(): @@ -78,21 +78,21 @@ def write_packages_to_zipapp(base, dist, modules, packages, zip_app): has.add(dest_str) if "/tests/" in dest_str or "/docs/" in dest_str: continue - print(dest_str) + print(dest_str) # noqa: T201 content = wheel_zip.read(filename) zip_app.writestr(dest_str, content) del content class WheelDownloader: - def __init__(self, into): + def __init__(self, into) -> None: if into.exists(): shutil.rmtree(into) into.mkdir(parents=True) self.into = into self.collected = defaultdict(lambda: defaultdict(dict)) self.pip_cmd = [str(Path(sys.executable).parent / "pip")] - self._cmd = self.pip_cmd + ["download", "-q", "--no-deps", "--dest", str(self.into)] + self._cmd = [*self.pip_cmd, "download", "-q", "--no-deps", "--dest", str(self.into)] def run(self, target, versions): whl = self.build_sdist(target) @@ -106,7 +106,8 @@ def run(self, target, versions): whl = self._get_wheel(dep, platform[2:] if platform and platform.startswith("==") else None, version) if whl is None: if dep_str not in wheel_store: - raise RuntimeError(f"failed to get {dep_str}, have {wheel_store}") + msg = f"failed to get {dep_str}, have {wheel_store}" + raise RuntimeError(msg) whl = wheel_store[dep_str] else: wheel_store[dep_str] = whl @@ -116,12 +117,19 @@ def run(self, target, versions): def _get_wheel(self, dep, platform, version): if isinstance(dep, Requirement): before = set(self.into.iterdir()) - if self._download(platform, False, "--python-version", version, "--only-binary", ":all:", str(dep)): - self._download(platform, True, "--python-version", version, str(dep)) + if self._download( + platform, + False, # noqa: FBT003 + "--python-version", + version, + "--only-binary", + ":all:", + str(dep), + ): + self._download(platform, True, "--python-version", version, str(dep)) # noqa: FBT003 after = set(self.into.iterdir()) new_files = after - before - # print(dep, new_files) - assert len(new_files) <= 1 + assert len(new_files) <= 1 # noqa: S101 if not len(new_files): return None new_file = next(iter(new_files)) @@ -129,7 +137,7 @@ def _get_wheel(self, dep, platform, version): return new_file dep = new_file new_file = self.build_sdist(dep) - assert new_file.suffix == ".whl" + assert new_file.suffix == ".whl" # noqa: S101 return new_file def _download(self, platform, stop_print_on_fail, *args): @@ -150,12 +158,14 @@ def get_dependencies(whl, version): for dep in deps: req = Requirement(dep) markers = getattr(req.marker, "_markers", ()) or () - if any(m for m in markers if isinstance(m, tuple) and len(m) == 3 and m[0].value == "extra"): + if any( + m for m in markers if isinstance(m, tuple) and len(m) == 3 and m[0].value == "extra" # noqa: PLR2004 + ): continue - py_versions = WheelDownloader._marker_at(markers, "python_version") + py_versions = WheelDownloader._marker_at(markers, "python_version") # noqa: SLF001 if py_versions: marker = Marker('python_version < "1"') - marker._markers = [ + marker._markers = [ # noqa: SLF001 markers[ver] for ver in sorted(i for i in set(py_versions) | {i - 1 for i in py_versions} if i >= 0) ] matches_python = marker.evaluate({"python_version": version}) @@ -163,13 +173,13 @@ def get_dependencies(whl, version): continue deleted = 0 for ver in py_versions: - deleted += WheelDownloader._del_marker_at(markers, ver - deleted) + deleted += WheelDownloader._del_marker_at(markers, ver - deleted) # noqa: SLF001 platforms = [] - platform_positions = WheelDownloader._marker_at(markers, "sys_platform") + platform_positions = WheelDownloader._marker_at(markers, "sys_platform") # noqa: SLF001 deleted = 0 for pos in platform_positions: # can only be ore meaningfully platform = f"{markers[pos][1].value}{markers[pos][2].value}" - deleted += WheelDownloader._del_marker_at(markers, pos - deleted) + deleted += WheelDownloader._del_marker_at(markers, pos - deleted) # noqa: SLF001 platforms.append(platform) if not platforms: platforms.append(None) @@ -180,7 +190,7 @@ def get_dependencies(whl, version): def _marker_at(markers, key): positions = [] for i, m in enumerate(markers): - if isinstance(m, tuple) and len(m) == 3 and m[0].value == key: + if isinstance(m, tuple) and len(m) == 3 and m[0].value == key: # noqa: PLR2004 positions.append(i) return positions @@ -208,7 +218,7 @@ def build_sdist(self, target): return self._build_sdist(self.into, folder) finally: # permission error on Windows <3.7 https://bugs.python.org/issue26660 - def onerror(func, path, exc_info): # noqa: U100 + def onerror(func, path, exc_info): # noqa: ARG001 os.chmod(path, S_IWUSR) func(path) @@ -219,14 +229,14 @@ def onerror(func, path, exc_info): # noqa: U100 def _build_sdist(self, folder, target): if not folder.exists() or not list(folder.iterdir()): - cmd = self.pip_cmd + ["wheel", "-w", str(folder), "--no-deps", str(target), "-q"] + cmd = [*self.pip_cmd, "wheel", "-w", str(folder), "--no-deps", str(target), "-q"] run_suppress_output(cmd, stop_print_on_fail=True) return list(folder.iterdir())[0] -def run_suppress_output(cmd, stop_print_on_fail=False): +def run_suppress_output(cmd, stop_print_on_fail=False): # noqa: FBT002 process = subprocess.Popen( - cmd, + cmd, # noqa: S603 stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, @@ -234,11 +244,11 @@ def run_suppress_output(cmd, stop_print_on_fail=False): ) out, err = process.communicate() if stop_print_on_fail and process.returncode != 0: - print(f"exit with {process.returncode} of {' '.join(quote(i) for i in cmd)}", file=sys.stdout) + print(f"exit with {process.returncode} of {' '.join(quote(i) for i in cmd)}", file=sys.stdout) # noqa: T201 if out: - print(out, file=sys.stdout) + print(out, file=sys.stdout) # noqa: T201 if err: - print(err, file=sys.stderr) + print(err, file=sys.stderr) # noqa: T201 raise SystemExit(process.returncode) return process.returncode @@ -251,24 +261,24 @@ def get_wheels_for_support_versions(folder): for pkg, platform_to_wheel in collected.items(): name = Requirement(pkg).name for platform, wheel in platform_to_wheel.items(): - platform = platform or "==any" - wheel_versions = packages[name][platform][wheel.name] + pl = platform or "==any" + wheel_versions = packages[name][pl][wheel.name] wheel_versions.versions.append(version) wheel_versions.wheel = wheel for name, p_w_v in packages.items(): for platform, w_v in p_w_v.items(): - print(f"{name} - {platform}") + print(f"{name} - {platform}") # noqa: T201 for wheel, wheel_versions in w_v.items(): - print(f"{' '.join(wheel_versions.versions)} of {wheel} (use {wheel_versions.wheel})") + print(f"{' '.join(wheel_versions.versions)} of {wheel} (use {wheel_versions.wheel})") # noqa: T201 return packages class WheelForVersion: - def __init__(self, wheel=None, versions=None): + def __init__(self, wheel=None, versions=None) -> None: self.wheel = wheel self.versions = versions if versions else [] - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({self.wheel!r}, {self.versions!r})" diff --git a/tasks/release.py b/tasks/release.py index 0d5ac3823..21c92ec3f 100644 --- a/tasks/release.py +++ b/tasks/release.py @@ -1,4 +1,4 @@ -"""Handles creating a release PR""" +"""Handles creating a release PR.""" from __future__ import annotations from pathlib import Path @@ -15,19 +15,20 @@ def main(version_str: str) -> None: repo = Repo(str(ROOT_SRC_DIR)) if repo.is_dirty(): - raise RuntimeError("Current repository is dirty. Please commit any changes and try again.") + msg = "Current repository is dirty. Please commit any changes and try again." + raise RuntimeError(msg) upstream, release_branch = create_release_branch(repo, version) release_commit = release_changelog(repo, version) tag = tag_release_commit(release_commit, repo, version) - print("push release commit") + print("push release commit") # noqa: T201 repo.git.push(upstream.name, release_branch) - print("push release tag") + print("push release tag") # noqa: T201 repo.git.push(upstream.name, tag) - print("All done! ✨ 🍰 ✨") + print("All done! ✨ 🍰 ✨") # noqa: T201 def create_release_branch(repo: Repo, version: Version) -> tuple[Remote, Head]: - print("create release branch from upstream main") + print("create release branch from upstream main") # noqa: T201 upstream = get_upstream(repo) upstream.fetch() branch_name = f"release-{version}" @@ -46,25 +47,24 @@ def get_upstream(repo: Repo) -> Remote: if url.endswith(upstream_remote): return remote urls.add(url) - raise RuntimeError(f"could not find {upstream_remote} remote, has {urls}") + msg = f"could not find {upstream_remote} remote, has {urls}" + raise RuntimeError(msg) def release_changelog(repo: Repo, version: Version) -> Commit: - print("generate release commit") - check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) - release_commit = repo.index.commit(f"release {version}") - return release_commit + print("generate release commit") # noqa: T201 + check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S603, S607 + return repo.index.commit(f"release {version}") def tag_release_commit(release_commit, repo, version) -> TagReference: - print("tag release commit") + print("tag release commit") # noqa: T201 existing_tags = [x.name for x in repo.tags] if version in existing_tags: - print(f"delete existing tag {version}") + print(f"delete existing tag {version}") # noqa: T201 repo.delete_tag(version) - print(f"create tag {version}") - tag = repo.create_tag(version, ref=release_commit, force=True) - return tag + print(f"create tag {version}") # noqa: T201 + return repo.create_tag(version, ref=release_commit, force=True) if __name__ == "__main__": diff --git a/tasks/update_embedded.py b/tasks/update_embedded.py index 3f159f0d2..e97d90d83 100755 --- a/tasks/update_embedded.py +++ b/tasks/update_embedded.py @@ -1,4 +1,4 @@ -"""Helper script to rebuild virtualenv.py from virtualenv_support""" +"""Helper script to rebuild virtualenv.py from virtualenv_support.""" # noqa: EXE002 from __future__ import annotations @@ -9,7 +9,7 @@ def crc32(data): - """Python version idempotent""" + """Python version idempotent.""" return _crc32(data.encode()) & 0xFFFFFFFF @@ -46,7 +46,7 @@ def rebuild(script_path): def handle_file(previous_content, filename, variable_name, previous_encoded): - print(f"Found file {filename}") + print(f"Found file {filename}") # noqa: T201 current_path = os.path.realpath(os.path.join(here, "..", "src", "virtualenv_embedded", filename)) _, file_type = os.path.splitext(current_path) keep_line_ending = file_type in (".bat",) @@ -55,26 +55,26 @@ def handle_file(previous_content, filename, variable_name, previous_encoded): current_crc = crc32(current_text) current_encoded = b64.encode(gzip.encode(current_text.encode())[0])[0].decode() if current_encoded == previous_encoded: - print(f" File up to date (crc: {current_crc:08x})") + print(f" File up to date (crc: {current_crc:08x})") # noqa: T201 return False, previous_content # Else: content has changed previous_text = gzip.decode(b64.decode(previous_encoded.encode())[0])[0].decode() previous_crc = crc32(previous_text) - print(f" Content changed (crc: {previous_crc:08x} -> {current_crc:08x})") + print(f" Content changed (crc: {previous_crc:08x} -> {current_crc:08x})") # noqa: T201 new_part = file_template.format(filename=filename, variable=variable_name, data=current_encoded) return True, new_part def report(exit_code, new, next_match, current, script_path): if new != current: - print("Content updated; overwriting... ", end="") + print("Content updated; overwriting... ", end="") # noqa: T201 with open(script_path, "w") as current_fh: current_fh.write(new) - print("done.") + print("done.") # noqa: T201 else: - print("No changes in content") + print("No changes in content") # noqa: T201 if next_match is None: - print("No variables were matched/found") + print("No variables were matched/found") # noqa: T201 raise SystemExit(exit_code) diff --git a/tasks/upgrade_wheels.py b/tasks/upgrade_wheels.py index beb2df746..daa289424 100644 --- a/tasks/upgrade_wheels.py +++ b/tasks/upgrade_wheels.py @@ -1,6 +1,4 @@ -""" -Helper script to rebuild virtualenv_support. Downloads the wheel files using pip -""" +"""Helper script to rebuild virtualenv_support. Downloads the wheel files using pip.""" from __future__ import annotations @@ -23,7 +21,7 @@ def download(ver, dest, package): subprocess.call( - [ + [ # noqa: S603 sys.executable, "-m", "pip", @@ -39,7 +37,7 @@ def download(ver, dest, package): ) -def run(): +def run(): # noqa: C901 old_batch = {i.name for i in DEST.iterdir() if i.suffix == ".whl"} with TemporaryDirectory() as temp: temp_path = Path(temp) @@ -56,7 +54,7 @@ def run(): thread.start() for thread in targets: thread.join() - new_batch = {i.name: i for f in folders.keys() for i in Path(f).iterdir()} + new_batch = {i.name: i for f in folders for i in Path(f).iterdir()} new_packages = new_batch.keys() - old_batch remove_packages = old_batch - new_batch.keys() @@ -82,8 +80,8 @@ def run(): lines.append(f"Removed {key} of {fmt_version(versions)}") lines.append("") changelog = "\n".join(lines) - print(changelog) - if len(lines) >= 4: + print(changelog) # noqa: T201 + if len(lines) >= 4: # noqa: PLR2004 (Path(__file__).parents[1] / "docs" / "changelog" / "u.bugfix.rst").write_text(changelog, encoding="utf-8") support_table = OrderedDict((".".join(str(j) for j in i), []) for i in SUPPORT) for package in sorted(new_batch.keys()): @@ -92,7 +90,7 @@ def run(): support_table[version].append(package) support_table = {k: OrderedDict((i.split("-")[0], i) for i in v) for k, v in support_table.items()} bundle = ",".join( - f"{v!r}: {{ {','.join(f'{p!r}: {f!r}' for p, f in l.items())} }}" for v, l in support_table.items() + f"{v!r}: {{ {','.join(f'{p!r}: {f!r}' for p, f in line.items())} }}" for v, line in support_table.items() ) msg = dedent( f""" @@ -102,7 +100,7 @@ def run(): BUNDLE_FOLDER = Path(__file__).absolute().parent BUNDLE_SUPPORT = {{ {bundle} }} - MAX = {repr(next(iter(support_table.keys())))} + MAX = {next(iter(support_table.keys()))!r} def get_embed_wheel(distribution, for_py_version): @@ -122,7 +120,7 @@ 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", "black", str(dest_target)]) + subprocess.run([sys.executable, "-m", "black", str(dest_target)]) # noqa: S603 raise SystemExit(outcome) @@ -135,7 +133,7 @@ def collect_package_versions(new_packages): result = defaultdict(list) for package in new_packages: split = package.split("-") - if len(split) < 2: + if len(split) < 2: # noqa: PLR2004 raise ValueError(package) key, version = split[0:2] result[key].append(version) diff --git a/tests/conftest.py b/tests/conftest.py index 800861e77..4d21fccc4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,8 +23,8 @@ def pytest_addoption(parser): def pytest_configure(config): """Ensure randomly is called before we re-order""" manager = config.pluginmanager - # noinspection PyProtectedMember - order = manager.hook.pytest_collection_modifyitems._nonwrappers + + order = manager.hook.pytest_collection_modifyitems._nonwrappers # noqa: SLF001 dest = next((i for i, p in enumerate(order) if p.plugin is manager.getplugin("randomly")), None) if dest is not None: from_pos = next(i for i, p in enumerate(order) if p.plugin is manager.getplugin(__file__)) @@ -47,7 +47,7 @@ def pytest_collection_modifyitems(config, items): @pytest.fixture(scope="session") -def has_symlink_support(tmp_path_factory): # noqa: U100 +def has_symlink_support(tmp_path_factory): # noqa: ARG001 return fs_supports_symlink() @@ -55,21 +55,19 @@ def has_symlink_support(tmp_path_factory): # noqa: U100 def link_folder(has_symlink_support): if has_symlink_support: return os.symlink - elif sys.platform == "win32": + if sys.platform == "win32": # on Windows junctions may be used instead import _winapi return getattr(_winapi, "CreateJunction", None) - else: - return None + return None @pytest.fixture(scope="session") def link_file(has_symlink_support): if has_symlink_support: return os.symlink - else: - return None + return None @pytest.fixture(scope="session") @@ -84,11 +82,10 @@ def _link(src, dest): else: shutil.copytree(s_src, s_dest) clean = partial(shutil.rmtree, str(dest)) + elif link_file: + link_file(s_src, s_dest) else: - if link_file: - link_file(s_src, s_dest) - else: - shutil.copy2(s_src, s_dest) + shutil.copy2(s_src, s_dest) return clean return _link @@ -148,9 +145,7 @@ def _ignore_global_config(tmp_path_factory): def _check_os_environ_stable(): old = os.environ.copy() # ensure we don't inherit parent env variables - to_clean = { - k for k in os.environ.keys() if k.startswith("VIRTUALENV_") or "VIRTUAL_ENV" in k or k.startswith("TOX_") - } + to_clean = {k for k in os.environ if k.startswith(("VIRTUALENV_", "TOX_")) or "VIRTUAL_ENV" in k} cleaned = {k: os.environ[k] for k, v in os.environ.items()} override = { "VIRTUALENV_NO_PERIODIC_UPDATE": "1", @@ -166,7 +161,7 @@ def _check_os_environ_stable(): raise finally: try: - for key in override.keys(): + for key in override: del os.environ[str(key)] if is_exception is False: new = os.environ @@ -250,11 +245,11 @@ class EnableCoverage: _COV_FILE = Path(coverage.__file__) _ROOT_COV_FILES_AND_FOLDERS = [i for i in _COV_FILE.parents[1].iterdir() if i.name.startswith("coverage")] - def __init__(self, link): + def __init__(self, link) -> None: self.link = link self.targets = [] - def __enter__(self, creator): + def __enter__(self, creator): # noqa: PLE0302 site_packages = creator.purelib for entry in self._ROOT_COV_FILES_AND_FOLDERS: target = site_packages / entry.name @@ -263,7 +258,7 @@ def __enter__(self, creator): self.targets.append((target, clean)) return self - def __exit__(self, exc_type, exc_val, exc_tb): # noqa: U100 + def __exit__(self, exc_type, exc_val, exc_tb): for target, clean in self.targets: if target.exists(): clean() @@ -295,8 +290,7 @@ def special_char_name(): @pytest.fixture() def special_name_dir(tmp_path, special_char_name): - dest = Path(str(tmp_path)) / special_char_name - return dest + return Path(str(tmp_path)) / special_char_name @pytest.fixture(scope="session") @@ -330,7 +324,7 @@ def change_env_var(key, value): yield finally: if already_set: - os.environ[key] = prev_value # type: ignore + os.environ[key] = prev_value else: del os.environ[key] # pragma: no cover diff --git a/tests/integration/test_run_int.py b/tests/integration/test_run_int.py index 8f4cc16d2..e1dc6d3f5 100644 --- a/tests/integration/test_run_int.py +++ b/tests/integration/test_run_int.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pathlib import Path +from typing import TYPE_CHECKING import pytest @@ -8,10 +8,13 @@ from virtualenv.info import IS_PYPY from virtualenv.util.subprocess import run_cmd +if TYPE_CHECKING: + from pathlib import Path + @pytest.mark.skipif(IS_PYPY, reason="setuptools distutils patching does not work") def test_app_data_pinning(tmp_path: Path) -> None: - version = "23.0" + version = "23.1" result = cli_run([str(tmp_path), "--pip", version, "--activators", "", "--seeder", "app-data"]) code, out, _ = run_cmd([str(result.creator.script("pip")), "list", "--disable-pip-version-check"]) assert not code diff --git a/tests/integration/test_zipapp.py b/tests/integration/test_zipapp.py index b2f27a01b..5bcddf519 100644 --- a/tests/integration/test_zipapp.py +++ b/tests/integration/test_zipapp.py @@ -2,6 +2,7 @@ import shutil import subprocess +from contextlib import suppress from pathlib import Path import pytest @@ -25,7 +26,7 @@ def zipapp_build_env(tmp_path_factory): # prefer CPython as builder as pypy is slow for impl in ["cpython", ""]: for version in range(11, 6, -1): - try: + with suppress(Exception): # create a virtual environment which is also guaranteed to contain a recent enough pip (bundled) session = cli_run( [ @@ -42,12 +43,11 @@ def zipapp_build_env(tmp_path_factory): exe = str(session.creator.exe) found = True break - except Exception: - pass if found: break else: - raise RuntimeError("could not find a python to build zipapp") + msg = "could not find a python to build zipapp" + raise RuntimeError(msg) cmd = [str(Path(exe).parent / "pip"), "install", "pip>=23", "packaging>=23"] subprocess.check_call(cmd) yield exe @@ -75,9 +75,9 @@ def zipapp_test_env(tmp_path_factory): @pytest.fixture() -def call_zipapp(zipapp, tmp_path, zipapp_test_env, temp_app_data): # noqa: U100 +def call_zipapp(zipapp, tmp_path, zipapp_test_env, temp_app_data): # noqa: ARG001 def _run(*args): - cmd = [str(zipapp_test_env), str(zipapp), "-vv", str(tmp_path / "env")] + list(args) + cmd = [str(zipapp_test_env), str(zipapp), "-vv", str(tmp_path / "env"), *list(args)] subprocess.check_call(cmd) return _run diff --git a/tests/unit/activation/conftest.py b/tests/unit/activation/conftest.py index 719bdffa0..64c481daf 100644 --- a/tests/unit/activation/conftest.py +++ b/tests/unit/activation/conftest.py @@ -15,7 +15,7 @@ class ActivationTester: - def __init__(self, of_class, session, cmd, activate_script, extension): + def __init__(self, of_class, session, cmd, activate_script, extension) -> None: # noqa: PLR0913 self.of_class = of_class self._creator = session.creator self._version_cmd = [cmd, "--version"] @@ -41,17 +41,18 @@ def get_version(self, raise_on_fail): encoding="utf-8", ) out, err = process.communicate() - result = out if out else err - self._version = result - return result - except Exception as exception: + except Exception as exception: # noqa: BLE001 self._version = exception if raise_on_fail: raise return RuntimeError(f"{self} is not available due {exception}") + else: + result = out if out else err + self._version = result + return result return self._version - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(\nversion={self._version!r},\ncreator={self._creator},\n" f"interpreter={self._creator.interpreter})" @@ -72,7 +73,7 @@ def __call__(self, monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) monkeypatch.delenv("VIRTUAL_ENV", raising=False) - invoke, env = self._invoke_script + [str(test_script)], self.env(tmp_path) + invoke, env = [*self._invoke_script, str(test_script)], self.env(tmp_path) try: process = Popen(invoke, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) @@ -81,24 +82,23 @@ def __call__(self, monkeypatch, tmp_path): except subprocess.CalledProcessError as exception: output = exception.output + exception.stderr assert not exception.returncode, output # noqa: PT017 - return + return None out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", raw, re.M).strip().splitlines() self.assert_output(out, raw, tmp_path) return env, activate_script def non_source_activate(self, activate_script): - return self._invoke_script + [str(activate_script)] + return [*self._invoke_script, str(activate_script)] - # noinspection PyMethodMayBeStatic - def env(self, tmp_path): # noqa: U100 + def env(self, tmp_path): # noqa: ARG002 env = os.environ.copy() # add the current python executable folder to the path so we already have another python on the path # also keep the path so the shells (fish, bash, etc can be discovered) env["PYTHONIOENCODING"] = "utf-8" - env["PATH"] = os.pathsep.join([dirname(sys.executable)] + env.get("PATH", "").split(os.pathsep)) + env["PATH"] = os.pathsep.join([dirname(sys.executable), *env.get("PATH", "").split(os.pathsep)]) # clear up some environment variables so they don't affect the tests - for key in [k for k in env.keys() if k.startswith("_OLD") or k.startswith("VIRTUALENV_")]: + for key in [k for k in env if k.startswith(("_OLD", "VIRTUALENV_"))]: del env[key] return env @@ -111,7 +111,7 @@ def _generate_test_script(self, activate_script, tmp_path): return test_script def _get_test_lines(self, activate_script): - commands = [ + return [ self.print_python_exe(), self.print_os_env_var("VIRTUAL_ENV"), self.activate_call(activate_script), @@ -125,7 +125,6 @@ def _get_test_lines(self, activate_script): self.print_os_env_var("VIRTUAL_ENV"), "", # just finish with an empty new line ] - return commands def assert_output(self, out, raw, tmp_path): # pre-activation @@ -187,7 +186,15 @@ def norm_path(path): class RaiseOnNonSourceCall(ActivationTester): - def __init__(self, of_class, session, cmd, activate_script, extension, non_source_fail_message): + def __init__( # noqa: PLR0913 + self, + of_class, + session, + cmd, + activate_script, + extension, + non_source_fail_message, + ) -> None: super().__init__(of_class, session, cmd, activate_script, extension) self.non_source_fail_message = non_source_fail_message diff --git a/tests/unit/activation/test_activator.py b/tests/unit/activation/test_activator.py index 9b15b0b46..b6b5b4986 100644 --- a/tests/unit/activation/test_activator.py +++ b/tests/unit/activation/test_activator.py @@ -7,7 +7,7 @@ def test_activator_prompt_cwd(monkeypatch, tmp_path): class FakeActivator(Activator): - def generate(self, creator): # noqa: U100 + def generate(self, creator): raise NotImplementedError cwd = tmp_path / "magic" diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py index 4a3676781..4c92b733c 100644 --- a/tests/unit/activation/test_bash.py +++ b/tests/unit/activation/test_bash.py @@ -9,7 +9,7 @@ @pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") def test_bash(raise_on_non_source_class, activation_tester): class Bash(raise_on_non_source_class): - def __init__(self, session): + def __init__(self, session) -> None: super().__init__( BashActivator, session, diff --git a/tests/unit/activation/test_batch.py b/tests/unit/activation/test_batch.py index 458278f0e..10dc41b1a 100644 --- a/tests/unit/activation/test_batch.py +++ b/tests/unit/activation/test_batch.py @@ -13,7 +13,7 @@ def test_batch(activation_tester_class, activation_tester, tmp_path): version_script.write_text("ver", encoding="utf-8") class Batch(activation_tester_class): - def __init__(self, session): + def __init__(self, session) -> None: super().__init__(BatchActivator, session, None, "activate.bat", "bat") self._version_cmd = [str(version_script)] self._invoke_script = [] @@ -24,7 +24,7 @@ def __init__(self, session): def _get_test_lines(self, activate_script): # for BATCH utf-8 support need change the character code page to 650001 - return ["@echo off", "", "chcp 65001 1>NUL"] + super()._get_test_lines(activate_script) + return ["@echo off", "", "chcp 65001 1>NUL", *super()._get_test_lines(activate_script)] def quote(self, s): """double quotes needs to be single, and single need to be double""" diff --git a/tests/unit/activation/test_csh.py b/tests/unit/activation/test_csh.py index e76b97b02..309ae811e 100644 --- a/tests/unit/activation/test_csh.py +++ b/tests/unit/activation/test_csh.py @@ -5,7 +5,7 @@ def test_csh(activation_tester_class, activation_tester): class Csh(activation_tester_class): - def __init__(self, session): + def __init__(self, session) -> None: super().__init__(CShellActivator, session, "csh", "activate.csh", "csh") def print_prompt(self): diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py index 2a82059e3..6a92a2790 100644 --- a/tests/unit/activation/test_fish.py +++ b/tests/unit/activation/test_fish.py @@ -14,7 +14,7 @@ def test_fish(activation_tester_class, activation_tester, monkeypatch, tmp_path) (fish_conf_dir / "config.fish").write_text("", encoding="utf-8") class Fish(activation_tester_class): - def __init__(self, session): + def __init__(self, session) -> None: super().__init__(FishActivator, session, "fish", "activate.fish", "fish") def print_prompt(self): diff --git a/tests/unit/activation/test_nushell.py b/tests/unit/activation/test_nushell.py index e3bd5a348..ed2ec79d7 100644 --- a/tests/unit/activation/test_nushell.py +++ b/tests/unit/activation/test_nushell.py @@ -8,7 +8,7 @@ def test_nushell(activation_tester_class, activation_tester): class Nushell(activation_tester_class): - def __init__(self, session): + def __init__(self, session) -> None: cmd = which("nu") if cmd is None and IS_WIN: cmd = "c:\\program files\\nu\\bin\\nu.exe" diff --git a/tests/unit/activation/test_powershell.py b/tests/unit/activation/test_powershell.py index 887b7666c..c454d6943 100644 --- a/tests/unit/activation/test_powershell.py +++ b/tests/unit/activation/test_powershell.py @@ -13,7 +13,7 @@ def test_powershell(activation_tester_class, activation_tester, monkeypatch): monkeypatch.setenv("TERM", "xterm") class PowerShell(activation_tester_class): - def __init__(self, session): + def __init__(self, session) -> None: cmd = "powershell.exe" if sys.platform == "win32" else "pwsh" super().__init__(PowerShellActivator, session, cmd, "activate.ps1", "ps1") self._version_cmd = [cmd, "-c", "$PSVersionTable"] diff --git a/tests/unit/activation/test_python_activator.py b/tests/unit/activation/test_python_activator.py index ee0d1eb42..17cda6815 100644 --- a/tests/unit/activation/test_python_activator.py +++ b/tests/unit/activation/test_python_activator.py @@ -11,7 +11,7 @@ def test_python(raise_on_non_source_class, activation_tester): class Python(raise_on_non_source_class): - def __init__(self, session): + def __init__(self, session) -> None: super().__init__( PythonActivator, session, @@ -25,7 +25,7 @@ def __init__(self, session): def env(self, tmp_path): env = os.environ.copy() env["PYTHONIOENCODING"] = "utf-8" - for key in {"VIRTUAL_ENV", "PYTHONPATH"}: + for key in ("VIRTUAL_ENV", "PYTHONPATH"): env.pop(str(key), None) env["PATH"] = os.pathsep.join([str(tmp_path), str(tmp_path / "other")]) return env @@ -57,10 +57,9 @@ def print_r(value): import pydoc_test print_r(pydoc_test.__file__) """ - result = dedent(raw).splitlines() - return result + return dedent(raw).splitlines() - def assert_output(self, out, raw, tmp_path): # noqa: U100 + def assert_output(self, out, raw, tmp_path): # noqa: ARG002 out = [literal_eval(i) for i in out] assert out[0] is None # start with VIRTUAL_ENV None @@ -69,7 +68,7 @@ def assert_output(self, out, raw, tmp_path): # noqa: U100 assert out[3] == str(self._creator.dest) # VIRTUAL_ENV now points to the virtual env folder new_path = out[4] # PATH now starts with bin path of current - assert ([str(self._creator.bin_dir)] + prev_path) == new_path + assert ([str(self._creator.bin_dir), *prev_path]) == new_path # sys path contains the site package at its start new_sys_path = out[5] @@ -85,7 +84,6 @@ def assert_output(self, out, raw, tmp_path): # noqa: U100 def non_source_activate(self, activate_script): act = str(activate_script) - cmd = self._invoke_script + ["-c", f"exec(open({act!r}).read())"] - return cmd + return [*self._invoke_script, "-c", f"exec(open({act!r}).read())"] activation_tester(Python) diff --git a/tests/unit/config/test___main__.py b/tests/unit/config/test___main__.py index a850415af..a4d0b7d34 100644 --- a/tests/unit/config/test___main__.py +++ b/tests/unit/config/test___main__.py @@ -2,13 +2,16 @@ import re import sys -from pathlib import Path from subprocess import PIPE, Popen, check_output +from typing import TYPE_CHECKING import pytest from virtualenv.__main__ import run_with_catch -from virtualenv.util.error import ProcessCallFailed +from virtualenv.util.error import ProcessCallFailedError + +if TYPE_CHECKING: + from pathlib import Path def test_main(): @@ -40,7 +43,7 @@ def _session_via_cli(args, options=None, setup_logging=True, env=None): def test_fail_no_traceback(raise_on_session_done, tmp_path, capsys): - raise_on_session_done(ProcessCallFailed(code=2, out="out\n", err="err\n", cmd=["something"])) + raise_on_session_done(ProcessCallFailedError(code=2, out="out\n", err="err\n", cmd=["something"])) with pytest.raises(SystemExit) as context: run_with_catch([str(tmp_path)]) assert context.value.code == 2 @@ -55,15 +58,15 @@ def test_fail_with_traceback(raise_on_session_done, tmp_path, capsys): with pytest.raises(TypeError, match="something bad"): run_with_catch([str(tmp_path), "--with-traceback"]) out, err = capsys.readouterr() - assert out == "" - assert err == "" + assert not out + assert not err @pytest.mark.usefixtures("session_app_data") def test_session_report_full(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: run_with_catch([str(tmp_path), "--setuptools", "bundle", "--wheel", "bundle"]) out, err = capsys.readouterr() - assert err == "" + assert not err lines = out.splitlines() regexes = [ r"created virtual environment .* in \d+ms", @@ -85,7 +88,7 @@ def _match_regexes(lines, regexes): def test_session_report_minimal(tmp_path, capsys): run_with_catch([str(tmp_path), "--activators", "", "--without-pip"]) out, err = capsys.readouterr() - assert err == "" + assert not err lines = out.splitlines() regexes = [ r"created virtual environment .* in \d+ms", diff --git a/tests/unit/config/test_env_var.py b/tests/unit/config/test_env_var.py index b8eb6561c..751944bb4 100644 --- a/tests/unit/config/test_env_var.py +++ b/tests/unit/config/test_env_var.py @@ -77,7 +77,7 @@ def test_extra_search_dir_via_env_var(tmp_path, monkeypatch): def test_value_alias(monkeypatch, mocker): from virtualenv.config.cli.parser import VirtualEnvConfigParser - prev = VirtualEnvConfigParser._fix_default + prev = VirtualEnvConfigParser._fix_default # noqa: SLF001 def func(self, action): if action.dest == "symlinks": diff --git a/tests/unit/create/conftest.py b/tests/unit/create/conftest.py index 5bf764ef5..58d390c5c 100644 --- a/tests/unit/create/conftest.py +++ b/tests/unit/create/conftest.py @@ -19,23 +19,20 @@ CURRENT = PythonInfo.current_system() -# noinspection PyUnusedLocal -def root(tmp_path_factory, session_app_data): # noqa: U100 +def root(tmp_path_factory, session_app_data): # noqa: ARG001 return CURRENT.system_executable def venv(tmp_path_factory, session_app_data): if CURRENT.is_venv: return sys.executable - else: - root_python = root(tmp_path_factory, session_app_data) - dest = tmp_path_factory.mktemp("venv") - process = Popen([str(root_python), "-m", "venv", "--without-pip", str(dest)]) - process.communicate() - # sadly creating a virtual environment does not tell us where the executable lives in general case - # so discover using some heuristic - exe_path = CURRENT.discover_exe(prefix=str(dest)).original_executable - return exe_path + root_python = root(tmp_path_factory, session_app_data) + dest = tmp_path_factory.mktemp("venv") + process = Popen([str(root_python), "-m", "venv", "--without-pip", str(dest)]) + process.communicate() + # sadly creating a virtual environment does not tell us where the executable lives in general case + # so discover using some heuristic + return CURRENT.discover_exe(prefix=str(dest)).original_executable PYTHON = { diff --git a/tests/unit/create/console_app/demo/__init__.py b/tests/unit/create/console_app/demo/__init__.py index f68e2c7e9..d7f2575eb 100644 --- a/tests/unit/create/console_app/demo/__init__.py +++ b/tests/unit/create/console_app/demo/__init__.py @@ -2,7 +2,7 @@ def run(): - print("magic") + print("magic") # noqa: T201 if __name__ == "__main__": diff --git a/tests/unit/create/console_app/demo/__main__.py b/tests/unit/create/console_app/demo/__main__.py index f68e2c7e9..d7f2575eb 100644 --- a/tests/unit/create/console_app/demo/__main__.py +++ b/tests/unit/create/console_app/demo/__main__.py @@ -2,7 +2,7 @@ def run(): - print("magic") + print("magic") # noqa: T201 if __name__ == "__main__": diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py index 8b3372920..b1f2921ce 100644 --- a/tests/unit/create/test_creator.py +++ b/tests/unit/create/test_creator.py @@ -55,22 +55,21 @@ def test_destination_exists_file(tmp_path, capsys): target = tmp_path / "out" target.write_text("", encoding="utf-8") err = _non_success_exit_code(capsys, str(target)) - msg = f"the destination {str(target)} already exists and is a file" + msg = f"the destination {target!s} already exists and is a file" assert msg in err, err @pytest.mark.skipif(sys.platform == "win32", reason="Windows only applies R/O to files") def test_destination_not_write_able(tmp_path, capsys): - if hasattr(os, "geteuid"): - if os.geteuid() == 0: - pytest.skip("no way to check permission restriction when running under root") + if hasattr(os, "geteuid") and os.geteuid() == 0: + pytest.skip("no way to check permission restriction when running under root") target = tmp_path prev_mod = target.stat().st_mode target.chmod(S_IREAD | S_IRGRP | S_IROTH) try: err = _non_success_exit_code(capsys, str(target)) - msg = f"the destination . is not write-able at {str(target)}" + msg = f"the destination . is not write-able at {target!s}" assert msg in err, err finally: target.chmod(prev_mod) @@ -84,8 +83,7 @@ def cleanup_sys_path(paths): if os.environ.get("PYCHARM_HELPERS_DIR"): to_remove.append(Path(os.environ["PYCHARM_HELPERS_DIR"]).parent) to_remove.append(Path(os.path.expanduser("~")) / ".PyCharm") - result = [i for i in paths if not any(str(i).startswith(str(t)) for t in to_remove)] - return result + return [i for i in paths if not any(str(i).startswith(str(t)) for t in to_remove)] @pytest.fixture(scope="session") @@ -93,7 +91,7 @@ def system(session_app_data): return get_env_debug_info(Path(CURRENT.system_executable), DEBUG_SCRIPT, session_app_data, os.environ) -CURRENT_CREATORS = [i for i in CURRENT.creators().key_to_class.keys() if i != "builtin"] +CURRENT_CREATORS = [i for i in CURRENT.creators().key_to_class if i != "builtin"] CREATE_METHODS = [] for k, v in CURRENT.creators().key_to_meta.items(): if k in CURRENT_CREATORS: @@ -107,7 +105,14 @@ def system(session_app_data): ("creator", "isolated"), [pytest.param(*i, id=f"{'-'.join(i[0])}-{i[1]}") for i in product(CREATE_METHODS, ["isolated", "global"])], ) -def test_create_no_seed(python, creator, isolated, system, coverage_env, special_name_dir): +def test_create_no_seed( # noqa: C901, PLR0912, PLR0913, PLR0915 + python, + creator, + isolated, + system, + coverage_env, + special_name_dir, +): dest = special_name_dir creator_key, method = creator cmd = [ @@ -150,7 +155,7 @@ def test_create_no_seed(python, creator, isolated, system, coverage_env, special # ensure all additional paths are related to the virtual environment for path in our_paths: msg = "\n".join(str(p) for p in system_sys_path) - msg = f"\n{str(path)}\ndoes not start with {str(dest)}\nhas:\n{msg}" + msg = f"\n{path!s}\ndoes not start with {dest!s}\nhas:\n{msg}" assert str(path).startswith(str(dest)), msg # ensure there's at least a site-packages folder as part of the virtual environment added assert any(p for p in our_paths if p.parts[-1] == "site-packages"), our_paths_repr @@ -159,7 +164,7 @@ def test_create_no_seed(python, creator, isolated, system, coverage_env, special global_sys_path = system_sys_path[-1] if isolated == "isolated": msg = "\n".join(str(j) for j in sys_path) - msg = f"global sys path {str(global_sys_path)} is in virtual environment sys path:\n{msg}" + msg = f"global sys path {global_sys_path!s} is in virtual environment sys path:\n{msg}" assert global_sys_path not in sys_path, msg else: common = [] @@ -234,9 +239,8 @@ def test_create_vcs_ignore_exists_override(tmp_path): @pytest.mark.skipif(not CURRENT.has_venv, reason="requires interpreter with venv") def test_venv_fails_not_inline(tmp_path, capsys, mocker): - if hasattr(os, "geteuid"): - if os.geteuid() == 0: - pytest.skip("no way to check permission restriction when running under root") + if hasattr(os, "geteuid") and os.geteuid() == 0: + pytest.skip("no way to check permission restriction when running under root") def _session_via_cli(args, options=None, setup_logging=True, env=None): session = session_via_cli(args, options, setup_logging, env) @@ -290,10 +294,9 @@ def test_prompt_set(tmp_path, creator, prompt): cfg = PyEnvCfg.from_file(result.creator.pyenv_cfg.path) if prompt is None: assert "prompt" not in cfg - else: - if creator != "venv": - assert "prompt" in cfg, list(cfg.content.keys()) - assert cfg["prompt"] == actual_prompt + elif creator != "venv": + assert "prompt" in cfg, list(cfg.content.keys()) + assert cfg["prompt"] == actual_prompt @pytest.mark.parametrize("creator", CURRENT_CREATORS) @@ -602,7 +605,7 @@ def _get_sys_path(flag=None): str(result.creator.purelib), str(result.creator.bin_dir), str(tmp_path / "base"), - f"{str(tmp_path / 'base_sep')}{os.sep}", + f"{tmp_path / 'base_sep'!s}{os.sep}", "name", f"name{os.sep}", f"{tmp_path.parent}{f'{tmp_path.name}_suffix'}", @@ -615,9 +618,9 @@ def _get_sys_path(flag=None): extra_all = _get_sys_path(None if python_path_on else "-E") if python_path_on: - assert extra_all[0] == "" # the cwd is always injected at start as '' + assert not extra_all[0] # the cwd is always injected at start as '' extra_all = extra_all[1:] - assert base[0] == "" + assert not base[0] base = base[1:] assert not (set(base) - set(extra_all)) # all base paths are present @@ -641,7 +644,7 @@ def _get_sys_path(flag=None): def test_venv_creator_without_write_perms(tmp_path, mocker): from virtualenv.run.session import Session - prev = Session._create + prev = Session._create # noqa: SLF001 def func(self): prev(self) diff --git a/tests/unit/create/via_global_ref/builtin/testing/path.py b/tests/unit/create/via_global_ref/builtin/testing/path.py index 96c5e3b42..3b64c6388 100644 --- a/tests/unit/create/via_global_ref/builtin/testing/path.py +++ b/tests/unit/create/via_global_ref/builtin/testing/path.py @@ -17,7 +17,8 @@ class FakeDataABC(metaclass=ABCMeta): @abstractmethod def filelist(self): """To mock a dir, just mock any child file.""" - raise NotImplementedError("Collection of (str) file paths to mock") + msg = "Collection of (str) file paths to mock" + raise NotImplementedError(msg) @property def fake_files(self): @@ -65,7 +66,8 @@ def resolve(self): def iterdir(self): if not self.is_dir(): - raise FileNotFoundError(f"No such mocked dir: '{self}'") + msg = f"No such mocked dir: '{self}'" + raise FileNotFoundError(msg) yield from map(self.joinpath, self.contained_fake_names) diff --git a/tests/unit/create/via_global_ref/builtin/testing/py_info.py b/tests/unit/create/via_global_ref/builtin/testing/py_info.py index f43b45d7d..27a660c4a 100644 --- a/tests/unit/create/via_global_ref/builtin/testing/py_info.py +++ b/tests/unit/create/via_global_ref/builtin/testing/py_info.py @@ -10,12 +10,12 @@ def fixture_file(fixture_name): files = Path(__file__).parent.parent.rglob(file_mask) try: return next(files) - except StopIteration: + except StopIteration as exc: # Fixture file was not found in the testing root and its subdirs. error = FileNotFoundError - raise error(file_mask) + raise error(file_mask) from exc def read_fixture(fixture_name): fixture_json = fixture_file(fixture_name).read_text(encoding="utf-8") - return PythonInfo._from_json(fixture_json) + return PythonInfo._from_json(fixture_json) # noqa: SLF001 diff --git a/tests/unit/create/via_global_ref/test_build_c_ext.py b/tests/unit/create/via_global_ref/test_build_c_ext.py index a2442d141..db1c16eb0 100644 --- a/tests/unit/create/via_global_ref/test_build_c_ext.py +++ b/tests/unit/create/via_global_ref/test_build_c_ext.py @@ -35,7 +35,7 @@ def builtin_shows_marker_missing(): not Path(CURRENT.system_include).exists() and not builtin_shows_marker_missing(), reason="Building C-Extensions requires header files with host python", ) -@pytest.mark.parametrize("creator", [i for i in CREATOR_CLASSES.keys() if i != "builtin"]) +@pytest.mark.parametrize("creator", [i for i in CREATOR_CLASSES if i != "builtin"]) def test_can_build_c_extensions(creator, tmp_path, coverage_env): env, greet = tmp_path / "env", str(tmp_path / "greet") shutil.copytree(str(Path(__file__).parent.resolve() / "greet"), greet) diff --git a/tests/unit/discovery/py_info/test_py_info.py b/tests/unit/discovery/py_info/test_py_info.py index f839e221f..260c95ee9 100644 --- a/tests/unit/discovery/py_info/test_py_info.py +++ b/tests/unit/discovery/py_info/test_py_info.py @@ -23,7 +23,7 @@ def test_current_as_json(): - result = CURRENT._to_json() + result = CURRENT._to_json() # noqa: SLF001 parsed = json.loads(result) a, b, c, d, e = sys.version_info assert parsed["version_info"] == {"major": a, "minor": b, "micro": c, "releaselevel": d, "serial": e} @@ -197,7 +197,15 @@ def test_py_info_cached_symlink(mocker, tmp_path, session_app_data): ), ], ) -def test_system_executable_no_exact_match(target, discovered, position, tmp_path, mocker, caplog, session_app_data): +def test_system_executable_no_exact_match( # noqa: PLR0913 + target, + discovered, + position, + tmp_path, + mocker, + caplog, + session_app_data, +): """Here we should fallback to other compatible""" caplog.set_level(logging.DEBUG) @@ -227,8 +235,7 @@ def _make_py_info(of): mocker.patch.object(target_py_info, "_find_possible_exe_names", return_value=names) mocker.patch.object(target_py_info, "_find_possible_folders", return_value=[str(tmp_path)]) - # noinspection PyUnusedLocal - def func(k, app_data, resolve_to_host, raise_on_error, env): # noqa: U100 + def func(k, app_data, resolve_to_host, raise_on_error, env): # noqa: ARG001 return discovered_with_path[k] mocker.patch.object(target_py_info, "from_exe", side_effect=func) @@ -236,7 +243,7 @@ def func(k, app_data, resolve_to_host, raise_on_error, env): # noqa: U100 target_py_info.system_executable = None target_py_info.executable = str(tmp_path) - mapped = target_py_info._resolve_to_system(session_app_data, target_py_info) + mapped = target_py_info._resolve_to_system(session_app_data, target_py_info) # noqa: SLF001 assert mapped.system_executable == CURRENT.system_executable found = discovered_with_path[mapped.base_executable] assert found is selected @@ -382,7 +389,7 @@ def test_fallback_existent_system_executable(mocker): # that "python" is not required and the standard `make install` does not provide one # Falsify some data to look like we're in a venv - current.prefix = current.exec_prefix = "/tmp/tmp.izZNCyINRj/venv" + current.prefix = current.exec_prefix = "/tmp/tmp.izZNCyINRj/venv" # noqa: S108 current.executable = current.original_executable = os.path.join(current.prefix, "bin/python") # Since we don't know if the distribution we're on provides python, use a binary that should not exist @@ -390,7 +397,7 @@ def test_fallback_existent_system_executable(mocker): mocker.patch.object(sys, "executable", current.executable) # ensure it falls back to an alternate binary name that exists - current._fast_get_system_executable() + current._fast_get_system_executable() # noqa: SLF001 assert os.path.basename(current.system_executable) in [ f"python{v}" for v in (current.version_info.major, f"{current.version_info.major}.{current.version_info.minor}") ] diff --git a/tests/unit/discovery/py_info/test_py_info_exe_based_of.py b/tests/unit/discovery/py_info/test_py_info_exe_based_of.py index 562424ef8..c843ca7be 100644 --- a/tests/unit/discovery/py_info/test_py_info_exe_based_of.py +++ b/tests/unit/discovery/py_info/test_py_info_exe_based_of.py @@ -26,7 +26,7 @@ def test_discover_empty_folder(tmp_path, session_app_data): @pytest.mark.parametrize("arch", [CURRENT.architecture, ""]) @pytest.mark.parametrize("version", [".".join(str(i) for i in CURRENT.version_info[0:i]) for i in range(3, 0, -1)]) @pytest.mark.parametrize("impl", [CURRENT.implementation, "python"]) -def test_discover_ok(tmp_path, suffix, impl, version, arch, into, caplog, session_app_data): +def test_discover_ok(tmp_path, suffix, impl, version, arch, into, caplog, session_app_data): # noqa: PLR0913 caplog.set_level(logging.DEBUG) folder = tmp_path / into folder.mkdir(parents=True, exist_ok=True) @@ -51,6 +51,6 @@ def test_discover_ok(tmp_path, suffix, impl, version, arch, into, caplog, sessio assert "get interpreter info via cmd: " in caplog.text dest.rename(dest.parent / (dest.name + "-1")) - CURRENT._cache_exe_discovery.clear() + CURRENT._cache_exe_discovery.clear() # noqa: SLF001 with pytest.raises(RuntimeError): CURRENT.discover_exe(session_app_data, inside_folder) diff --git a/tests/unit/discovery/test_discovery.py b/tests/unit/discovery/test_discovery.py index b8c820ad6..f5eb17cbb 100644 --- a/tests/unit/discovery/test_discovery.py +++ b/tests/unit/discovery/test_discovery.py @@ -33,7 +33,7 @@ def test_discovery_via_path(monkeypatch, case, tmp_path, caplog, session_app_dat pyvenv_cfg = Path(sys.executable).parents[1] / "pyvenv.cfg" if pyvenv_cfg.exists(): (target / pyvenv_cfg.name).write_bytes(pyvenv_cfg.read_bytes()) - new_path = os.pathsep.join([str(target)] + os.environ.get("PATH", "").split(os.pathsep)) + new_path = os.pathsep.join([str(target), *os.environ.get("PATH", "").split(os.pathsep)]) monkeypatch.setenv("PATH", new_path) interpreter = get_interpreter(core, []) diff --git a/tests/unit/discovery/windows/conftest.py b/tests/unit/discovery/windows/conftest.py index 08c72fd6b..8959ee9e2 100644 --- a/tests/unit/discovery/windows/conftest.py +++ b/tests/unit/discovery/windows/conftest.py @@ -7,12 +7,12 @@ @pytest.fixture() -def _mock_registry(mocker): +def _mock_registry(mocker): # noqa: C901 from virtualenv.discovery.windows.pep514 import winreg loc, glob = {}, {} mock_value_str = (Path(__file__).parent / "winreg-mock-values.py").read_text(encoding="utf-8") - exec(mock_value_str, glob, loc) + exec(mock_value_str, glob, loc) # noqa: S102 enum_collect = loc["enum_collect"] value_collect = loc["value_collect"] key_open = loc["key_open"] @@ -37,13 +37,13 @@ def _query_value_ex(key, value_name): mocker.patch.object(winreg, "QueryValueEx", side_effect=_query_value_ex) class Key: - def __init__(self, value): + def __init__(self, value) -> None: self.value = value def __enter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): # noqa: U100 + def __exit__(self, exc_type, exc_val, exc_tb): return None @contextmanager @@ -99,4 +99,4 @@ def _populate_pyinfo_cache(monkeypatch): ] for _, major, minor, arch, exe, _ in interpreters: info = _mock_pyinfo(major, minor, arch, exe) - monkeypatch.setitem(virtualenv.discovery.cached_py_info._CACHE, Path(info.executable), info) + monkeypatch.setitem(virtualenv.discovery.cached_py_info._CACHE, Path(info.executable), info) # noqa: SLF001 diff --git a/tests/unit/discovery/windows/test_windows_pep514.py b/tests/unit/discovery/windows/test_windows_pep514.py index 0b392d6b9..0d36840ba 100644 --- a/tests/unit/discovery/windows/test_windows_pep514.py +++ b/tests/unit/discovery/windows/test_windows_pep514.py @@ -32,7 +32,7 @@ def test_pep514(): def test_pep514_run(capsys, caplog): from virtualenv.discovery.windows import pep514 - pep514._run() + pep514._run() # noqa: SLF001 out, err = capsys.readouterr() expected = textwrap.dedent( r""" diff --git a/tests/unit/seed/embed/test_base_embed.py b/tests/unit/seed/embed/test_base_embed.py index a249e478a..255e03177 100644 --- a/tests/unit/seed/embed/test_base_embed.py +++ b/tests/unit/seed/embed/test_base_embed.py @@ -1,19 +1,22 @@ from __future__ import annotations import sys -from pathlib import Path +from typing import TYPE_CHECKING import pytest from virtualenv.run import session_via_cli +if TYPE_CHECKING: + from pathlib import Path + @pytest.mark.parametrize( ("args", "download"), [([], False), (["--no-download"], False), (["--never-download"], False), (["--download"], True)], ) def test_download_cli_flag(args, download, tmp_path): - session = session_via_cli(args + [str(tmp_path)]) + session = session_via_cli([*args, str(tmp_path)]) assert session.seeder.download is download diff --git a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py b/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py index fac9ac699..7db52e1f1 100644 --- a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py +++ b/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py @@ -3,13 +3,12 @@ import contextlib import os import sys -from pathlib import Path from stat import S_IWGRP, S_IWOTH, S_IWUSR from subprocess import Popen, check_call from threading import Thread +from typing import TYPE_CHECKING import pytest -from pytest_mock import MockerFixture from virtualenv.discovery import cached_py_info from virtualenv.discovery.py_info import PythonInfo @@ -18,6 +17,11 @@ from virtualenv.seed.wheels.embed import BUNDLE_FOLDER, BUNDLE_SUPPORT from virtualenv.util.path import safe_delete +if TYPE_CHECKING: + from pathlib import Path + + from pytest_mock import MockerFixture + @pytest.mark.slow() @pytest.mark.parametrize("copies", [False, True] if fs_supports_symlink() else [True]) @@ -106,7 +110,7 @@ def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies) # Windows does not allow removing a executable while running it, so when uninstalling pip we need to do it via # python -m pip remove_cmd = [str(result.creator.exe), "-m", "pip"] + remove_cmd[1:] - process = Popen(remove_cmd + ["pip", "wheel"]) + process = Popen([*remove_cmd, "pip", "wheel"]) _, __ = process.communicate() assert not process.returncode # pip is greedy here, removing all packages removes the site-package too @@ -124,16 +128,16 @@ def read_only_dir(d): for root, _, filenames in os.walk(str(d)): os.chmod(root, os.stat(root).st_mode & ~write) for filename in filenames: - filename = os.path.join(root, filename) - os.chmod(filename, os.stat(filename).st_mode & ~write) + name = os.path.join(root, filename) + os.chmod(name, os.stat(name).st_mode & ~write) try: yield finally: for root, _, filenames in os.walk(str(d)): os.chmod(root, os.stat(root).st_mode | write) for filename in filenames: - filename = os.path.join(root, filename) - os.chmod(filename, os.stat(filename).st_mode | write) + name = os.path.join(root, filename) + os.chmod(name, os.stat(name).st_mode | write) @pytest.fixture() @@ -167,12 +171,12 @@ def test_populated_read_only_cache_and_symlinked_app_data(tmp_path, current_fast assert cli_run(cmd) check_call((str(dest.joinpath("bin/python")), "-c", "import pip")) - cached_py_info._CACHE.clear() # necessary to re-trigger py info discovery + cached_py_info._CACHE.clear() # necessary to re-trigger py info discovery # noqa: SLF001 safe_delete(dest) # should succeed with special flag when read-only with read_only_dir(temp_app_data): - assert cli_run(["--read-only-app-data"] + cmd) + assert cli_run(["--read-only-app-data", *cmd]) check_call((str(dest.joinpath("bin/python")), "-c", "import pip")) @@ -192,12 +196,12 @@ def test_populated_read_only_cache_and_copied_app_data(tmp_path, current_fastest assert cli_run(cmd) - cached_py_info._CACHE.clear() # necessary to re-trigger py info discovery + cached_py_info._CACHE.clear() # necessary to re-trigger py info discovery # noqa: SLF001 safe_delete(dest) # should succeed with special flag when read-only with read_only_dir(temp_app_data): - assert cli_run(["--read-only-app-data"] + cmd) + assert cli_run(["--read-only-app-data", *cmd]) @pytest.mark.slow() @@ -233,7 +237,7 @@ def _run_parallel_threads(tmp_path): def _run(name): try: cli_run(["--seeder", "app-data", str(tmp_path / name), "--no-pip", "--no-setuptools", "--wheel", "bundle"]) - except Exception as exception: + except Exception as exception: # noqa: BLE001 as_str = str(exception) exceptions.append(as_str) diff --git a/tests/unit/seed/embed/test_pip_invoke.py b/tests/unit/seed/embed/test_pip_invoke.py index 9deda2614..b12d85a5d 100644 --- a/tests/unit/seed/embed/test_pip_invoke.py +++ b/tests/unit/seed/embed/test_pip_invoke.py @@ -14,7 +14,7 @@ @pytest.mark.slow() @pytest.mark.parametrize("no", ["pip", "setuptools", "wheel", ""]) -def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_fastest, no): +def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_fastest, no): # noqa: C901 extra_search_dir = tmp_path / "extra" extra_search_dir.mkdir() for_py_version = f"{sys.version_info.major}.{sys.version_info.minor}" @@ -22,7 +22,7 @@ def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_f for wheel_filename in BUNDLE_SUPPORT[for_py_version].values(): copy2(str(BUNDLE_FOLDER / wheel_filename), str(extra_search_dir)) - def _load_embed_wheel(app_data, distribution, for_py_version, version): # noqa: U100 + 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" @@ -34,9 +34,7 @@ def _execute(cmd, env): for distribution, with_version in versions.items(): if distribution == no: continue - if with_version == "embed": - expected.add(BUNDLE_FOLDER) - elif old[distribution] == new[distribution]: + if with_version == "embed" or old[distribution] == new[distribution]: expected.add(BUNDLE_FOLDER) else: expected.add(extra_search_dir) @@ -49,7 +47,7 @@ def _execute(cmd, env): assert found == expected_list return original(cmd, env) - original = PipInvoke._execute + original = PipInvoke._execute # noqa: SLF001 run = mocker.patch.object(PipInvoke, "_execute", side_effect=_execute) versions = {"pip": "embed", "setuptools": "bundle", "wheel": new["wheel"].split("-")[1]} diff --git a/tests/unit/seed/wheels/test_acquire.py b/tests/unit/seed/wheels/test_acquire.py index dc471a144..caa6c1b84 100644 --- a/tests/unit/seed/wheels/test_acquire.py +++ b/tests/unit/seed/wheels/test_acquire.py @@ -2,14 +2,12 @@ import os import sys -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from subprocess import CalledProcessError -from typing import Callable -from unittest.mock import MagicMock +from typing import TYPE_CHECKING, Callable import pytest -from pytest_mock import MockerFixture from virtualenv.app_data import AppDataDiskFolder from virtualenv.seed.wheels.acquire import download_wheel, get_wheel, pip_wheel_env_run @@ -17,6 +15,11 @@ from virtualenv.seed.wheels.periodic_update import dump_datetime from virtualenv.seed.wheels.util import Wheel, discover_wheels +if TYPE_CHECKING: + from unittest.mock import MagicMock + + from pytest_mock import MockerFixture + @pytest.fixture(autouse=True) def _fake_release_date(mocker): @@ -62,7 +65,7 @@ def test_download_fails(mocker, for_py_version, session_app_data): as_path = mocker.MagicMock() with pytest.raises(CalledProcessError) as context: - download_wheel("pip", "==1", for_py_version, [], session_app_data, as_path, os.environ), + download_wheel("pip", "==1", for_py_version, [], session_app_data, as_path, os.environ) exc = context.value assert exc.output == "out" assert exc.stderr == "err" @@ -123,7 +126,7 @@ def test_get_wheel_download_cached( downloaded_wheel: tuple[Wheel, MagicMock], time_freeze: Callable[[datetime], None], ) -> None: - time_freeze(datetime.now()) + time_freeze(datetime.now(tz=timezone.utc)) from virtualenv.app_data.via_disk_folder import JSONStoreDisk app_data = AppDataDiskFolder(folder=str(tmp_path)) @@ -150,7 +153,7 @@ def test_get_wheel_download_cached( { "filename": expected.name, "release_date": None, - "found_date": dump_datetime(datetime.now()), + "found_date": dump_datetime(datetime.now(tz=timezone.utc)), "source": "download", }, ], diff --git a/tests/unit/seed/wheels/test_bundle.py b/tests/unit/seed/wheels/test_bundle.py index fbd34f9ce..d0e95cf31 100644 --- a/tests/unit/seed/wheels/test_bundle.py +++ b/tests/unit/seed/wheels/test_bundle.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path import pytest @@ -25,7 +25,7 @@ def next_pip_wheel(for_py_version): @pytest.fixture(scope="module") def app_data(tmp_path_factory, for_py_version, next_pip_wheel): temp_folder = tmp_path_factory.mktemp("module-app-data") - now = dump_datetime(datetime.now()) + now = dump_datetime(datetime.now(tz=timezone.utc)) app_data_ = AppDataDiskFolder(str(temp_folder)) app_data_.embed_update_log("pip", for_py_version).write( { diff --git a/tests/unit/seed/wheels/test_periodic_update.py b/tests/unit/seed/wheels/test_periodic_update.py index d4a5e0998..e4988b5cf 100644 --- a/tests/unit/seed/wheels/test_periodic_update.py +++ b/tests/unit/seed/wheels/test_periodic_update.py @@ -6,7 +6,7 @@ import sys from collections import defaultdict from contextlib import contextmanager -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from io import StringIO from itertools import zip_longest from pathlib import Path @@ -42,9 +42,21 @@ def _clear_pypi_info_cache(): def test_manual_upgrade(session_app_data, caplog, mocker, for_py_version): wheel = get_embed_wheel("pip", for_py_version) - new_version = NewVersion(wheel.path, datetime.now(), datetime.now() - timedelta(days=20), "manual") + new_version = NewVersion( + wheel.path, + datetime.now(tz=timezone.utc), + datetime.now(tz=timezone.utc) - timedelta(days=20), + "manual", + ) - def _do_update(distribution, for_py_version, embed_filename, app_data, search_dirs, periodic): # noqa: U100 + def _do_update( # noqa: PLR0913 + distribution, + for_py_version, # noqa: ARG001 + embed_filename, # noqa: ARG001 + app_data, # noqa: ARG001 + search_dirs, # noqa: ARG001 + periodic, # noqa: ARG001 + ): if distribution == "pip": return [new_version] return [] @@ -70,9 +82,9 @@ def _do_update(distribution, for_py_version, embed_filename, app_data, search_di def test_pick_periodic_update(tmp_path, mocker, for_py_version): embed, current = get_embed_wheel("setuptools", "3.6"), get_embed_wheel("setuptools", for_py_version) mocker.patch("virtualenv.seed.wheels.bundle.load_embed_wheel", return_value=embed) - completed = datetime.now() - timedelta(days=29) + completed = datetime.now(tz=timezone.utc) - timedelta(days=29) u_log = UpdateLog( - started=datetime.now() - timedelta(days=30), + started=datetime.now(tz=timezone.utc) - timedelta(days=30), completed=completed, versions=[NewVersion(filename=current.path, found_date=completed, release_date=completed, source="periodic")], periodic=True, @@ -102,7 +114,7 @@ def test_pick_periodic_update(tmp_path, mocker, for_py_version): def test_periodic_update_stops_at_current(mocker, session_app_data, for_py_version): current = get_embed_wheel("setuptools", for_py_version) - now, completed = datetime.now(), datetime.now() - timedelta(days=29) + now, completed = datetime.now(tz=timezone.utc), datetime.now(tz=timezone.utc) - timedelta(days=29) u_log = UpdateLog( started=completed, completed=completed, @@ -122,7 +134,7 @@ def test_periodic_update_stops_at_current(mocker, session_app_data, for_py_versi def test_periodic_update_latest_per_patch(mocker, session_app_data, for_py_version): current = get_embed_wheel("setuptools", for_py_version) expected_path = wheel_path(current, (0, 1, 2)) - now = datetime.now() + now = datetime.now(tz=timezone.utc) completed = now - timedelta(hours=2) u_log = UpdateLog( started=completed, @@ -143,7 +155,7 @@ def test_periodic_update_latest_per_patch(mocker, session_app_data, for_py_versi def test_periodic_update_latest_per_patch_prev_is_manual(mocker, session_app_data, for_py_version): current = get_embed_wheel("setuptools", for_py_version) expected_path = wheel_path(current, (0, 1, 2)) - now = datetime.now() + now = datetime.now(tz=timezone.utc) completed = now - timedelta(hours=2) u_log = UpdateLog( started=completed, @@ -165,7 +177,7 @@ def test_periodic_update_latest_per_patch_prev_is_manual(mocker, session_app_dat def test_manual_update_honored(mocker, session_app_data, for_py_version): current = get_embed_wheel("setuptools", for_py_version) expected_path = wheel_path(current, (0, 1, 1)) - now = datetime.now() + now = datetime.now(tz=timezone.utc) completed = now u_log = UpdateLog( started=completed, @@ -190,7 +202,7 @@ def wheel_path(wheel, of, pre_release=""): return str(wheel.path.parent / new_name) -_UP_NOW = datetime.now() +_UP_NOW = datetime.now(tz=timezone.utc) _UPDATE_SKIP = { "started_just_now_no_complete": UpdateLog(started=_UP_NOW, completed=None, versions=[], periodic=True), "started_1_hour_no_complete": UpdateLog( @@ -372,14 +384,14 @@ def test_do_update_first(tmp_path, mocker, time_freeze): ] download_wheels = (Wheel(Path(i[0])) for i in pip_version_remote) - def _download_wheel( + def _download_wheel( # noqa: PLR0913 distribution, - version_spec, # noqa: U100 + version_spec, # noqa: ARG001 for_py_version, search_dirs, app_data, to_folder, - env, # noqa: U100 + env, # noqa: ARG001 ): assert distribution == "pip" assert for_py_version == "3.9" @@ -441,14 +453,14 @@ def test_do_update_skip_already_done(tmp_path, mocker, time_freeze): extra = tmp_path / "extra" extra.mkdir() - def _download_wheel( - distribution, # noqa: U100 - version_spec, # noqa: U100 - for_py_version, # noqa: U100 - search_dirs, # noqa: U100 - app_data, # noqa: U100 - to_folder, # noqa: U100 - env, # noqa: U100 + def _download_wheel( # noqa: PLR0913 + distribution, # noqa: ARG001 + version_spec, # noqa: ARG001 + for_py_version, # noqa: ARG001 + search_dirs, # noqa: ARG001 + app_data, # noqa: ARG001 + to_folder, # noqa: ARG001 + env, # noqa: ARG001 ): return wheel.path @@ -490,15 +502,15 @@ def _download_wheel( def test_new_version_eq(): - value = NewVersion("a", datetime.now(), datetime.now(), "periodic") + value = NewVersion("a", datetime.now(tz=timezone.utc), datetime.now(tz=timezone.utc), "periodic") assert value == value def test_new_version_ne(): - assert NewVersion("a", datetime.now(), datetime.now(), "periodic") != NewVersion( + assert NewVersion("a", datetime.now(tz=timezone.utc), datetime.now(tz=timezone.utc), "periodic") != NewVersion( "a", - datetime.now(), - datetime.now() + timedelta(hours=1), + datetime.now(tz=timezone.utc), + datetime.now(tz=timezone.utc) + timedelta(hours=1), "manual", ) @@ -508,7 +520,8 @@ def test_get_release_unsecure(mocker, caplog): def _release(of, context): assert of == "https://pypi.org/pypi/pip/json" if context is None: - raise URLError("insecure") + msg = "insecure" + raise URLError(msg) assert context yield StringIO(json.dumps({"releases": {"20.1": [{"upload_time": "2020-12-22T12:12:12"}]}})) @@ -516,7 +529,7 @@ def _release(of, context): result = release_date_for_wheel_path(Path("pip-20.1.whl")) - assert result == datetime(year=2020, month=12, day=22, hour=12, minute=12, second=12) + assert result == datetime(year=2020, month=12, day=22, hour=12, minute=12, second=12, tzinfo=timezone.utc) assert url_o.call_count == 2 assert "insecure" in caplog.text assert " failed " in caplog.text @@ -544,7 +557,7 @@ def download(): do = download() return mocker.patch( "virtualenv.seed.wheels.acquire.download_wheel", - side_effect=lambda *a, **k: next(do), # noqa: U100 + side_effect=lambda *a, **k: next(do), # noqa: ARG005 ) @@ -638,7 +651,7 @@ def test_download_periodic_stop_at_first_usable(tmp_path, mocker, time_freeze): rel_date_gen = iter(rel_date_remote) release_date = mocker.patch( "virtualenv.seed.wheels.periodic_update.release_date_for_wheel_path", - side_effect=lambda *a, **k: next(rel_date_gen), # noqa: U100 + side_effect=lambda *a, **k: next(rel_date_gen), # noqa: ARG005 ) last_update = _UP_NOW - timedelta(days=14) @@ -670,7 +683,7 @@ def test_download_periodic_stop_at_first_usable_with_previous_minor(tmp_path, mo rel_date_gen = iter(rel_date_remote) release_date = mocker.patch( "virtualenv.seed.wheels.periodic_update.release_date_for_wheel_path", - side_effect=lambda *a, **k: next(rel_date_gen), # noqa: U100 + side_effect=lambda *a, **k: next(rel_date_gen), # noqa: ARG005 ) last_update = _UP_NOW - timedelta(days=14) diff --git a/tests/unit/seed/wheels/test_wheels_util.py b/tests/unit/seed/wheels/test_wheels_util.py index 00afa040a..87edab2d4 100644 --- a/tests/unit/seed/wheels/test_wheels_util.py +++ b/tests/unit/seed/wheels/test_wheels_util.py @@ -10,7 +10,7 @@ def test_wheel_support_no_python_requires(mocker): wheel = get_embed_wheel("setuptools", for_py_version=None) zip_mock = mocker.MagicMock() mocker.patch("virtualenv.seed.wheels.util.ZipFile", new=zip_mock) - zip_mock.return_value.__enter__.return_value.read = lambda name: b"" # noqa: U100 + zip_mock.return_value.__enter__.return_value.read = lambda name: b"" # noqa: ARG005 supports = wheel.support_py("3.8") assert supports is True diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 1959c5f13..2ad1d8f4c 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -34,5 +34,5 @@ def recreate_target_file(): for task in tasks: try: task.result() - except Exception: + except Exception: # noqa: BLE001 pytest.fail(traceback.format_exc()) diff --git a/tox.ini b/tox.ini index 6a9fb2736..ed3a6567f 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ requires = tox>=4.2 env_list = fix + py312 py311 py310 py39 @@ -41,7 +42,7 @@ commands = description = format the code base to adhere to our styles, and complain about what we cannot do automatically skip_install = true deps = - pre-commit>=3.2.2 + pre-commit>=3.3.2 commands = pre-commit run --all-files --show-diff-on-failure @@ -80,7 +81,7 @@ description = do a release, required posarg of the version number deps = gitpython>=3.1.31 packaging>=23.1 - towncrier>=22.12 + towncrier>=23.6 change_dir = {toxinidir}/tasks commands = python release.py --version {posargs}