Skip to content

Fix file scripts not copied to bin on poetry install#10736

Open
Mr-Neutr0n wants to merge 2 commits intopython-poetry:mainfrom
Mr-Neutr0n:fix/file-scripts-install
Open

Fix file scripts not copied to bin on poetry install#10736
Mr-Neutr0n wants to merge 2 commits intopython-poetry:mainfrom
Mr-Neutr0n:fix/file-scripts-install

Conversation

@Mr-Neutr0n
Copy link

@Mr-Neutr0n Mr-Neutr0n commented Feb 13, 2026

Summary

Fixes #10664

When pyproject.toml defines scripts with type = "file":

[tool.poetry.scripts]
my-command = { reference = "my-script.sh", type = "file" }

poetry install does not copy the referenced file into the virtualenv's bin/ directory. Only poetry build handles these correctly (via WheelBuilder._copy_file_scripts). Running poetry run my-command after install produces a warning about the script not being installed.

Changes

  • src/poetry/masonry/builders/editable.py: After processing console script entry points in _add_scripts(), added a second pass that iterates over [tool.poetry.scripts] entries, identifies those with type = "file", validates that the referenced file exists, and copies it to the virtualenv's script directory with executable permissions. This mirrors what WheelBuilder._copy_file_scripts already does for built wheels.

  • Test fixture + test: Added tests/fixtures/file_scripts_project/ with a project that has both a file script and a console entry point, and a test that verifies both are installed correctly during an editable build.

Test plan

  • New test test_builder_installs_file_scripts verifies file scripts are copied to the venv bin directory with correct content and permissions
  • Existing tests continue to pass (console scripts, bad scripts, etc.)
  • Manual verification: create a project with type = "file" script, run poetry install, confirm the script appears in the venv's bin/

Summary by Sourcery

Ensure editable installs copy file-based scripts into the virtualenv alongside console entry points.

Bug Fixes:

  • Fix editable installations not copying file-based scripts defined in [tool.poetry.scripts] into the virtualenv bin directory.

Tests:

  • Add fixture project and test verifying file-based scripts and console entry points are installed with correct content and executability during editable builds.

When pyproject.toml defines scripts with type = "file" (e.g.,
my-command = { reference = "my-script.sh", type = "file" }), poetry
install now copies them into the virtualenv's bin/ directory, matching
the behavior of poetry build which already included them in wheels.

The EditableBuilder._add_scripts() method previously only handled
console_scripts entry points. This adds a second pass that iterates
over [tool.poetry.scripts], finds entries with type = "file", and
copies the referenced files to the scripts directory with proper
executable permissions.

Fixes python-poetry#10664
@sourcery-ai
Copy link

sourcery-ai bot commented Feb 13, 2026

Reviewer's Guide

Extends the editable builder to install [tool.poetry.scripts] entries defined as file-type scripts into the virtualenv’s bin directory, and adds tests/fixtures to verify both file and console scripts are correctly installed in editable builds.

Sequence diagram for installing file-type scripts during editable builds

sequenceDiagram
    actor Developer
    participant PoetryCLI
    participant EditableBuilder
    participant PoetryConfig
    participant FileSystem
    participant VirtualenvBin

    Developer->>PoetryCLI: poetry install
    PoetryCLI->>EditableBuilder: build_editable()
    EditableBuilder->>EditableBuilder: _add_scripts()

    Note over EditableBuilder: Existing behavior: add console scripts
    EditableBuilder->>PoetryConfig: get console_scripts
    PoetryConfig-->>EditableBuilder: console script entry points
    EditableBuilder->>VirtualenvBin: write console script wrappers

    Note over EditableBuilder: New behavior: handle file-type scripts
    EditableBuilder->>PoetryConfig: local_config.get(scripts)
    PoetryConfig-->>EditableBuilder: scripts mapping
    loop for each script entry
        EditableBuilder->>EditableBuilder: check specification type
        alt type == file
            EditableBuilder->>FileSystem: resolve source_path
            alt source_path does not exist
                EditableBuilder->>EditableBuilder: write_error_line(file missing)
            else source_path is not a file
                EditableBuilder->>EditableBuilder: write_error_line(not a file)
            else valid file script
                EditableBuilder->>VirtualenvBin: copy2(source_path, target)
                EditableBuilder->>VirtualenvBin: chmod(target, 0o755)
            end
        else other types
            EditableBuilder->>EditableBuilder: ignore in file pass
        end
    end

    EditableBuilder-->>PoetryCLI: list of added scripts
    PoetryCLI-->>Developer: editable installation complete
Loading

Class diagram for EditableBuilder handling of file-type scripts

classDiagram
    class EditableBuilder {
        - Poetry _poetry
        - Path _path
        - IO _io
        + list~Path~ _add_scripts()
        + void _add_dist_info(list~Path~ added_files)
    }

    class Poetry {
        + dict local_config
    }

    class IO {
        + void write_error_line(str message)
    }

    class Path {
        + bool exists()
        + bool is_file()
        + Path joinpath(str name)
    }

    class shutil {
        + void copy2(Path source_path, Path target)
    }

    class ScriptsDirectory {
        + Path scripts_path
        + void chmod(int mode)
    }

    EditableBuilder --> Poetry : uses
    EditableBuilder --> IO : uses
    EditableBuilder --> Path : uses
    EditableBuilder ..> shutil : uses
    EditableBuilder --> ScriptsDirectory : copies_file_scripts_to

    %% New logic inside _add_scripts
    %% 1. Iterate self._poetry.local_config.get("scripts", {})
    %% 2. For dict specifications with type == file
    %% 3. Build source_path = self._path / reference
    %% 4. Validate exists() and is_file()
    %% 5. Copy via shutil.copy2 to scripts_path.joinpath(name)
    %% 6. chmod(0o755) and append to added list
Loading

File-Level Changes

Change Details Files
EditableBuilder now installs file-type scripts into the virtualenv bin alongside console entry points.
  • Extend _add_scripts to iterate over [tool.poetry.scripts] entries after console script handling.
  • Filter script specifications for dict entries with type == 'file'.
  • Resolve the referenced file path relative to the project root and validate existence and that it is a regular file, emitting error lines otherwise.
  • Copy valid file scripts into the environment’s scripts/bin directory named by the script key, set executable permissions (0o755), and track them in the added list.
src/poetry/masonry/builders/editable.py
New fixture project and test ensure file scripts are copied and console entry points remain installed for editable builds.
  • Add file_scripts_project fixture with pyproject.toml defining both a file script and a console script.
  • Provide a bin/my-script.sh shell script used as the file script source and a minimal package exposing a main() console entry point.
  • Introduce pytest fixture to construct a Poetry object for the new fixture project via Factory().create_poetry.
  • Add test_builder_installs_file_scripts to build a temporary virtualenv, run EditableBuilder.build(), and assert that the file script is copied with matching content and executable bit, and that the console script also exists.
tests/masonry/builders/test_editable_builder.py
tests/fixtures/file_scripts_project/pyproject.toml
tests/fixtures/file_scripts_project/bin/my-script.sh
tests/fixtures/file_scripts_project/file_scripts_project/__init__.py

Assessment against linked issues

Issue Objective Addressed Explanation
#10664 Ensure that scripts defined under [tool.poetry.scripts] with type = "file" are installed by poetry install into the virtualenv's bin/ directory so they can be executed normally (e.g. via poetry run my-command) without the "not installed as a script" warning.
#10664 Validate file-script entries during install: if a type = "file" script references a non-existent or non-file path, surface a clear error/warning during the editable build, similar in spirit to the behavior of poetry build.
#10664 Avoid the KeyError ('callable') when running poetry run <file-script> by ensuring the script is properly installed so poetry run does not attempt to treat the script spec as a console entry point.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The new file-script handling in _add_scripts() duplicates much of WheelBuilder._copy_file_scripts; consider extracting a shared helper so editable and wheel builds use the same code path and stay in sync on future changes.
  • You call shutil.copy2() and then unconditionally chmod(0o755) on the target; consider either relying on the copied mode or aligning the permission logic explicitly with the wheel builder to avoid subtle differences in behavior between install modes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new file-script handling in `_add_scripts()` duplicates much of `WheelBuilder._copy_file_scripts`; consider extracting a shared helper so editable and wheel builds use the same code path and stay in sync on future changes.
- You call `shutil.copy2()` and then unconditionally `chmod(0o755)` on the target; consider either relying on the copied mode or aligning the permission logic explicitly with the wheel builder to avoid subtle differences in behavior between install modes.

## Individual Comments

### Comment 1
<location> `src/poetry/masonry/builders/editable.py:219-220` </location>
<code_context>
+        for name, specification in self._poetry.local_config.get(
+            "scripts", {}
+        ).items():
+            if isinstance(specification, dict) and specification.get("type") == "file":
+                source = specification["reference"]
+                source_path = self._path / source
+
</code_context>

<issue_to_address>
**issue:** Accessing `specification["reference"]` directly can raise a `KeyError` for malformed configs.

Because this directly indexes `specification["reference"]`, a script entry like `type = "file"` without `reference` will raise `KeyError` and stop processing all scripts. Consider using `specification.get("reference")` and surfacing a validation-style error (similar to the missing-file case) to keep behavior consistent and more robust against malformed configs.
</issue_to_address>

### Comment 2
<location> `tests/masonry/builders/test_editable_builder.py:433-442` </location>
<code_context>
+def test_builder_installs_file_scripts(
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for failure modes of file scripts (missing or non-file references).

The new logic logs errors and skips scripts when `reference` does not exist or is not a file, but those branches aren’t tested. Please add tests for a missing path and for a directory `reference`, asserting that the script is not created in the venv `bin` directory and that the expected error is written to `io` (similar to `test_builder_catches_bad_scripts_*`).

Suggested implementation:

```python
def test_builder_installs_file_scripts(
    file_scripts_poetry: Poetry,
    tmp_path: Path,
) -> None:
    env_manager = EnvManager(file_scripts_poetry)
    venv_path = tmp_path / "venv"
    env_manager.build_venv(venv_path)
    tmp_venv = VirtualEnv(venv_path)

    builder = EditableBuilder(file_scripts_poetry, tmp_venv, NullIO())
    builder.build()


def test_builder_skips_missing_file_script(
    fixture_dir: FixtureDirGetter,
    tmp_path: Path,
) -> None:
    poetry = Factory().create_poetry(fixture_dir("file_scripts_missing_reference_project"))
    env_manager = EnvManager(poetry)
    venv_path = tmp_path / "venv"
    env_manager.build_venv(venv_path)
    tmp_venv = VirtualEnv(venv_path)

    io = BufferedIO()
    builder = EditableBuilder(poetry, tmp_venv, io)
    builder.build()

    bin_dir = tmp_venv.path / ("Scripts" if os.name == "nt" else "bin")

    # script for the missing reference must not be created
    # (use the actual script name configured in the test fixture)
    missing_script = bin_dir / "missing_file_script"
    assert not missing_script.exists()

    output = io.fetch_output()
    # ensure the error about the missing file script reference is logged
    # (keep the substring aligned with the actual error message)
    assert "file script" in output
    assert "does not exist" in output


def test_builder_skips_directory_file_script(
    fixture_dir: FixtureDirGetter,
    tmp_path: Path,
) -> None:
    poetry = Factory().create_poetry(fixture_dir("file_scripts_directory_reference_project"))
    env_manager = EnvManager(poetry)
    venv_path = tmp_path / "venv"
    env_manager.build_venv(venv_path)
    tmp_venv = VirtualEnv(venv_path)

    io = BufferedIO()
    builder = EditableBuilder(poetry, tmp_venv, io)
    builder.build()

    bin_dir = tmp_venv.path / ("Scripts" if os.name == "nt" else "bin")

    # script for the directory reference must not be created
    # (use the actual script name configured in the test fixture)
    directory_script = bin_dir / "directory_file_script"
    assert not directory_script.exists()

    output = io.fetch_output()
    # ensure the error about the directory file script reference is logged
    # (keep the substring aligned with the actual error message)
    assert "file script" in output
    assert "is not a file" in output

```

1. These tests assume a `BufferedIO` (or similar) class is already used in this test module (e.g. in `test_builder_catches_bad_scripts_*`). If the name differs, replace `BufferedIO` with the existing IO test helper and adjust how output is retrieved (`fetch_output()` vs other method).
2. Create two new fixture projects under the existing `tests/fixtures` hierarchy (or wherever `fixture_dir` resolves):
   - `file_scripts_missing_reference_project`:
     - A `pyproject.toml` that defines at least one valid file script and one file script whose `reference` points to a non-existent file. The failing script’s console entry name should be `missing_file_script` (or update the test to the chosen name).
   - `file_scripts_directory_reference_project`:
     - A `pyproject.toml` that defines at least one valid file script and one file script whose `reference` points to a directory (e.g. a folder in the project). The failing script’s console entry name should be `directory_file_script` (or update the test accordingly).
3. Update the asserted substrings (`"file script"`, `"does not exist"`, `"is not a file"`) to match the exact error messages emitted by the new logic that handles invalid file script references, following the pattern used in `test_builder_catches_bad_scripts_*`.
4. If your test suite has a helper to compute the virtualenv bin path instead of duplicating the `("Scripts" if os.name == "nt" else "bin")` logic, replace that line with the shared helper for consistency.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Use specification.get('reference') instead of specification['reference']
to avoid KeyError on malformed configs. Add a validation error message
when the reference field is missing.

Add tests covering three failure modes:
- File script with non-existent reference path
- File script referencing a directory instead of a file
- File script missing the reference field entirely
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

file scripts are not copied to bin on poetry install

1 participant