Skip to content

Add SubprocessPlugin for subprocess.run and shutil.which interception#1

Merged
elijahr merged 4 commits intomainfrom
elijahr/subprocess-plugin
Mar 5, 2026
Merged

Add SubprocessPlugin for subprocess.run and shutil.which interception#1
elijahr merged 4 commits intomainfrom
elijahr/subprocess-plugin

Conversation

@elijahr
Copy link
Copy Markdown
Contributor

@elijahr elijahr commented Mar 5, 2026

Summary

  • Adds SubprocessPlugin that intercepts subprocess.run and shutil.which globally during a bigfoot sandbox using class-level reference counting (compatible with nested sandboxes)
  • Adds bigfoot.subprocess_mock module-level proxy that auto-creates SubprocessPlugin on the current test verifier on first access
  • Strict FIFO queue semantics for subprocess.run mocks: calls must match in registration order; mismatched or extra calls raise UnmockedInteractionError
  • Semi-permissive shutil.which mocking: unregistered names return None silently; registered names are tracked on the timeline
  • Conflict detection raises ConflictError if subprocess.run or shutil.which is already patched by a third party (e.g., unittest.mock, pytest-mock) when the bouncer activates

New API

  • subprocess_mock.mock_run(command, *, returncode, stdout, stderr, raises, required)
  • subprocess_mock.mock_which(name, returns, *, required=False)
  • subprocess_mock.install() — activates the bouncer without registering any mocks
  • subprocess_mock.run / subprocess_mock.which — sentinels for assert_interaction()

Test plan

  • Unit tests covering mock_run FIFO ordering, mock_which semi-permissive behavior, conflict detection, unused mock tracking, reference counting, format helpers
  • Full test suite passes (CI)
  • Ruff + mypy clean

Changes

  • src/bigfoot/plugins/subprocess.py — new plugin
  • src/bigfoot/__init__.py — exports subprocess_mock, adds SubprocessPlugin to __all__
  • tests/unit/test_init.py — updated __all__ assertion
  • docs/ — new guide and reference pages, updated index/nav
  • CHANGELOG.md — v0.3.0 entry
  • pyproject.toml — version bump 0.2.0 → 0.3.0

🤖 Generated with Claude Code

elijahr added 3 commits March 5, 2026 02:20
…eption

Adds a new bigfoot plugin that intercepts subprocess.run and shutil.which
at the module level using reference-counted class-level activation,
following the HttpPlugin pattern exactly.

Key behaviors:
- mock_run: strict FIFO queue - wrong command or empty queue raises
  UnmockedInteractionError immediately (bouncer guarantee)
- mock_which: semi-permissive - unregistered names return None silently;
  registered names tracked on timeline with required=False default
- assertable_fields: only {"command"} for run, {"name"} for which
- install(): no-op method to trigger proxy creation before sandbox entry
  for tests that want bouncer active with no mocks registered

Adds subprocess_mock singleton proxy to bigfoot.__init__ following the
same lazy-creation pattern as the existing http singleton.
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the bigfoot testing framework by introducing a dedicated SubprocessPlugin. This new plugin provides robust control over external process interactions and binary path lookups within test environments. It ensures that subprocess.run calls are strictly managed in a predictable order, while offering flexibility for shutil.which to handle optional binaries gracefully. The implementation also includes safeguards against conflicts with other mocking libraries, thereby improving test reliability and isolation for applications that interact with the system shell.

Highlights

  • New SubprocessPlugin: Introduced a new SubprocessPlugin that globally intercepts subprocess.run and shutil.which during bigfoot sandboxes, utilizing class-level reference counting for compatibility with nested sandboxes.
  • Strict FIFO for subprocess.run: subprocess.run mocks now adhere to strict FIFO queue semantics, meaning calls must match their registration order. Mismatched or extra calls will immediately raise an UnmockedInteractionError.
  • Semi-permissive shutil.which: shutil.which mocking is semi-permissive; unregistered names return None silently without raising errors, while registered names are tracked on the interaction timeline.
  • Conflict Detection: The plugin includes conflict detection, raising a ConflictError if subprocess.run or shutil.which are already patched by a third-party library (e.g., unittest.mock, pytest-mock) when the bigfoot bouncer activates.
  • New API Endpoints: Added new module-level proxy functions: subprocess_mock.mock_run, subprocess_mock.mock_which, and subprocess_mock.install(), along with subprocess_mock.run and subprocess_mock.which sentinels for assert_interaction().
  • Documentation and Version Update: Comprehensive documentation for the SubprocessPlugin has been added, including new guide and reference pages, and the project version has been bumped to 0.3.0.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • CHANGELOG.md
    • SubprocessPlugin added, intercepting subprocess.run (strict FIFO) and shutil.which (semi-permissive).
    • bigfoot.subprocess_mock proxy added for auto-creation of SubprocessPlugin.
    • subprocess_mock.mock_run for registering subprocess.run mocks.
    • subprocess_mock.mock_which for registering shutil.which mocks.
    • subprocess_mock.install() for activating the bouncer without mocks.
Activity
  • No specific activity (comments, reviews, progress) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new SubprocessPlugin for intercepting subprocess.run and shutil.which, along with comprehensive documentation and tests. The implementation is solid and follows the existing patterns in the library. I've found a couple of edge cases that are not handled correctly and suggested some minor refactorings for conciseness. Overall, this is a great addition to the library.

Comment on lines +283 to +286
def _which_interceptor(name: str, **kwargs: Any) -> str | None: # noqa: ANN401
verifier = _get_verifier_or_raise(_SOURCE_WHICH)
plugin = _find_subprocess_plugin(verifier)
return plugin._handle_which(name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The _which_interceptor correctly accepts extra keyword arguments via **kwargs, but it fails to pass them to plugin._handle_which(name). This means that if shutil.which is called with arguments like mode or path, they will be dropped.

To fix this, you should pass **kwargs to _handle_which and also update the signature of _handle_which at line 367 to accept them (e.g., def _handle_which(self, name: str, **kwargs: Any) -> str | None:).

Suggested change
def _which_interceptor(name: str, **kwargs: Any) -> str | None: # noqa: ANN401
verifier = _get_verifier_or_raise(_SOURCE_WHICH)
plugin = _find_subprocess_plugin(verifier)
return plugin._handle_which(name)
def _which_interceptor(name: str, **kwargs: Any) -> str | None: # noqa: ANN401
verifier = _get_verifier_or_raise(_SOURCE_WHICH)
plugin = _find_subprocess_plugin(verifier)
return plugin._handle_which(name, **kwargs)

Comment on lines +315 to +319
if args:
cmd = args[0]
else:
cmd = kwargs.get("args", [])
cmd_list = list(cmd)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The _handle_run method does not correctly handle the case where subprocess.run is called with a command as a string instead of a list of strings. The line cmd_list = list(cmd) will split a string command into a list of characters, which will then fail to match any mock configured with a list of strings. This can lead to confusing UnmockedInteractionErrors.

For example, subprocess.run("ls") would result in cmd_list being ['l', 's'], which would not match a mock like mock_run(["ls"]).

I suggest adding a check to explicitly disallow string commands. This would make the behavior clearer than the current silent mis-handling.

        if args:
            cmd = args[0]
        else:
            cmd = kwargs.get("args", [])

        if isinstance(cmd, str):
            raise TypeError("bigfoot.subprocess_mock only supports list-of-strings commands, not a single string.")

        cmd_list = list(cmd)

Comment thread src/bigfoot/__init__.py
Comment on lines +164 to +167
for p in verifier._plugins:
if isinstance(p, _SubprocessPlugin):
plugin = p
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This loop to find the plugin can be written more concisely using next() with a generator expression. This can improve readability and is a common Python idiom.

        plugin = next((p for p in verifier._plugins if isinstance(p, _SubprocessPlugin)), None)

Comment on lines +104 to +110
for plugin in verifier._plugins:
if isinstance(plugin, SubprocessPlugin):
return plugin
raise RuntimeError(
"BUG: bigfoot SubprocessPlugin interceptor is active but no "
"SubprocessPlugin is registered on the current verifier."
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This loop and subsequent raise can be simplified by using next() within a try...except StopIteration block. This is a more concise and idiomatic way to find the first item in a sequence or raise an error if it's not found.

Suggested change
for plugin in verifier._plugins:
if isinstance(plugin, SubprocessPlugin):
return plugin
raise RuntimeError(
"BUG: bigfoot SubprocessPlugin interceptor is active but no "
"SubprocessPlugin is registered on the current verifier."
)
try:
return next(p for p in verifier._plugins if isinstance(p, SubprocessPlugin))
except StopIteration:
raise RuntimeError(
"BUG: bigfoot SubprocessPlugin interceptor is active but no "
"SubprocessPlugin is registered on the current verifier."
) from None

- Remove quoted type annotation in assert_interaction (UP037)
- Use cast() in format_unused_mock_hint to properly type tuple destructuring
- Rename which-loop variable to avoid type collision with run-loop config
- Remove unused type: ignore[assignment] comments on subprocess.run assignments
- Shorten format_assert_hint lines via sm variable (E501)
- Remove unused imports from test file (F401)
- Add noqa: ANN401 to _handle_run for Any-typed *args/**kwargs
@elijahr elijahr merged commit 2f61cef into main Mar 5, 2026
10 checks passed
@elijahr elijahr deleted the elijahr/subprocess-plugin branch March 5, 2026 10:13
elijahr added a commit that referenced this pull request Mar 6, 2026
- SubprocessPlugin: pass **kwargs through _which_interceptor to
  _handle_which (shutil.which mode/path args were silently dropped)
- SubprocessPlugin: raise TypeError on string commands in _handle_run
  instead of silently splitting into characters
- Replace loop-and-break plugin lookups with idiomatic next() in
  subprocess, popen, and proxy code
- Extract _push_cm() helper in _BigfootModule to deduplicate
  __enter__/__aenter__ sandbox creation logic

Bump version to 0.10.1.
elijahr added a commit that referenced this pull request Mar 6, 2026
* fix: address Gemini Code Assist review feedback from PRs #1-3

- SubprocessPlugin: pass **kwargs through _which_interceptor to
  _handle_which (shutil.which mode/path args were silently dropped)
- SubprocessPlugin: raise TypeError on string commands in _handle_run
  instead of silently splitting into characters
- Replace loop-and-break plugin lookups with idiomatic next() in
  subprocess, popen, and proxy code
- Extract _push_cm() helper in _BigfootModule to deduplicate
  __enter__/__aenter__ sandbox creation logic

Bump version to 0.10.1.

* fix: resolve 14 mypy strict-mode errors

- Add explicit type annotations to avoid Returning Any errors
- Remove unused type: ignore comments
- Add import-untyped ignores for psycopg2 and asyncpg (no stubs available)
- Add assignment ignores for aiohttp URL vs str type mismatch
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.

1 participant