Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1319,3 +1319,48 @@ The option `--next-phase` allows the increment of prerelease phase versions.
* `--next-phase`: Increment the phase of the current version.
* `--short (-s)`: Output the version number only.
* `--dry-run`: Do not update pyproject.toml file.

## wrapper

The `wrapper` command generates a `poetryw` Bash script and a `poetry-wrapper.properties` file in the current directory
to pin a specific Poetry version for project-local execution. This ensures consistent Poetry behavior across
development, CI/CD, and production environments.

### Usage

```console
poetry wrapper [options]
```

### Options

* `--poetry-version=<version>`: Specifies the Poetry version to pin (e.g., 2.1.2). Defaults to the current Poetry
version if not provided.

### Examples

Generate a wrapper using the current Poetry version:

```bash
poetry wrapper
```

Generate a wrapper for a specific Poetry version:

```bash
poetry wrapper --poetry-version=2.1.2
```

### Generated Files

* `poetryw`: A Bash script that installs and runs the pinned Poetry version. Run `./poetryw` to execute Poetry commands
with the specified version.
* `poetry-wrapper.properties`: Stores the pinned version (e.g., `version=2.1.2`).

### Notes

* The `poetryw` script is designed for **Unix-like systems (Linux, macOS)**. Windows support is not currently available
but may be added in the future.
* Existing `poetryw` or `poetry-wrapper.properties` files will be overwritten with a warning.
* The command validates that the version follows the `X.Y.Z` format but does not check if the version exists in a
registry.
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/poetry/console/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ def _load() -> Command:
"source add",
"source remove",
"source show",
# Wrapper command
"wrapper",
]

# these are special messages to override the default message when a command is not found
Expand Down
172 changes: 172 additions & 0 deletions src/poetry/console/commands/wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
from __future__ import annotations

import os
import re

from pathlib import Path
from typing import ClassVar

from cleo.io.inputs.option import Option

from poetry.__version__ import __version__
from poetry.console.commands.command import Command


class WrapperCommand(Command):
name = "wrapper"
description = "Generates a wrapper script to pin a specific Poetry version."
help = """
The <info>wrapper</info> command creates a <comment>poetryw</comment> script and a
<comment>poetry-wrapper.properties</comment> file in the current directory to ensure
Poetry commands use a specified version.

This is useful for projects that require a consistent Poetry version across environments.

<comment>Examples:</>
<info>poetry wrapper</info>
Generates a wrapper using the current Poetry version.
<info>poetry wrapper --poetry-version 2.1.2</info>
Generates a wrapper for Poetry version 2.1.2.

<comment>Note:</> The generated <comment>poetryw</comment> script is a Bash script and is
intended for Unix-like systems. Windows support may require additional configuration.
"""

options: ClassVar[list[Option]] = [
Option(
"--poetry-version",
description="Specify the Poetry version for the wrapper (defaults to current version)",
flag=False,
default=None,
),
]

def handle(self) -> int:
"""Generate the Poetry wrapper script and properties file."""
version: str = self.option("poetry-version") or __version__

# Validate version format (basic semantic version check)
if not self._is_valid_version(version):
self.line_error(
f"Invalid version format: <error>{version}</error>. Expected format: X.Y.Z"
)
return 1

project_dir: Path = (
Path.cwd()
) # Consider self.poetry.file.path.parent for project root
properties_file: Path = project_dir / "poetry-wrapper.properties"
script_file: Path = project_dir / "poetryw"

# Check for existing files and warn user
if properties_file.exists() or script_file.exists():
self.line(
"<warning>Warning: Existing wrapper files detected. They will be overwritten.</warning>"
)

# Write poetry-wrapper.properties
try:
properties_file.write_text(f"version={version}\n", encoding="utf-8")
except OSError as e:
self.line_error(
f"Failed to write <error>{properties_file}</error>: <error>{e}</error>"
)
return 1

# Generate the poetryw script content
script_content: str = self._generate_script_content(version)

# Write and make the script executable
try:
script_file.write_text(script_content, encoding="utf-8")
# Set executable permissions (Unix-like systems)
self._make_executable(script_file)
except OSError as e:
self.line_error(
f"Failed to write or configure <error>{script_file}</error>: <error>{e}</error>"
)
return 1

self.line(
f"Poetry wrapper generated successfully for version <info>{version}</info>."
)
self.line(f" - Properties: <comment>{properties_file}</comment>")
self.line(f" - Script: <comment>{script_file}</comment>")
self.line("Run <info>./poetryw</info> to use Poetry with the pinned version.")
return 0

def _is_valid_version(self, version: str) -> bool:
"""Validate that the version string follows a semantic version format (X.Y.Z).

Args:
version: The version string to validate.

Returns:
True if the version matches X.Y.Z, False otherwise.
"""
return bool(re.match(r"^\d+\.\d+\.\d+$", version))

def _generate_script_content(self, version: str) -> str:
"""Generate the content of the poetryw wrapper script."""
return f"""#!/bin/bash
# Poetry wrapper script
# Ensures Poetry version {version} is used to run commands
#
# Generated by `poetry wrapper` on {self._get_current_date()}

set -e # Exit on error

# Check if poetry-wrapper.properties exists
PROPERTIES_FILE="poetry-wrapper.properties"
if [ ! -f "$PROPERTIES_FILE" ]; then
echo "Error: $PROPERTIES_FILE not found. Run 'poetry wrapper' to generate it."
exit 1
fi

# Read version from poetry-wrapper.properties
POETRY_VERSION=$(grep '^version=' "$PROPERTIES_FILE" | cut -d'=' -f2)
if [ -z "$POETRY_VERSION" ]; then
echo "Error: No version specified in $PROPERTIES_FILE."
exit 1
fi

# Set POETRY_HOME to a version-specific cache
POETRY_HOME="$HOME/.poetry/versions/$POETRY_VERSION"

# Install Poetry if not present
if [ ! -f "$POETRY_HOME/bin/poetry" ]; then
echo "Installing Poetry version $POETRY_VERSION to $POETRY_HOME..."
mkdir -p "$POETRY_HOME"
if ! curl -sSL https://install.python-poetry.org | POETRY_HOME="$POETRY_HOME" python3 - --version "$POETRY_VERSION"; then
echo "Error: Failed to install Poetry version $POETRY_VERSION."
exit 1
fi
# Verify Poetry executable exists after installation
if [ ! -f "$POETRY_HOME/bin/poetry" ]; then
echo "Error: Poetry executable not found at $POETRY_HOME/bin/poetry after installation."
exit 1
fi
fi

# Run Poetry with provided arguments
exec "$POETRY_HOME/bin/poetry" "$@"
"""

def _make_executable(self, file: Path) -> None:
"""Set executable permissions for the script file on Unix-like systems."""
if os.name != "posix":
self.line(
"<warning>Setting executable permissions is not supported on this platform.</warning>"
)
return
try:
# Equivalent to chmod 755
file.chmod(0o755)
except OSError:
raise

def _get_current_date(self) -> str:
"""Return the current date for script metadata."""
from datetime import datetime

return datetime.now().strftime("%Y-%m-%d")
Loading