Skip to content

fix(script-runner): use plugin venv for Python suite analysis - #3767

Open
mcosgriff wants to merge 7 commits into
mainfrom
3732-python-script-suite-analysis-does-not-use-venv
Open

fix(script-runner): use plugin venv for Python suite analysis#3767
mcosgriff wants to merge 7 commits into
mainfrom
3732-python-script-suite-analysis-does-not-use-venv

Conversation

@mcosgriff

@mcosgriff mcosgriff commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Forward the selected virtual environment for temporary scripts and expose plugin site-packages when analyzing saved and temporary Python suites.

What changed

Test suite wasn't passing the virtual environment

Why it changed

Closes #3732

Testing strategy

Installed cowsay package in the test plugin venv, imported that library import cowsay and added print(cowsay.get_output_string('cow', 'Hello World')) to the setup method to the Power group

Review notes

Similar logic for running scripts in script runner

Forward the selected virtual environment for temporary scripts and expose
plugin site-packages when analyzing saved and temporary Python suites.
@mcosgriff mcosgriff linked an issue Aug 24, 2026 that may be closed by this pull request
Comment thread openc3/lib/openc3/utilities/script.rb Fixed
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.40%. Comparing base (4232f06) to head (e7d4e04).
⚠️ Report is 14 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3767      +/-   ##
==========================================
+ Coverage   79.38%   79.40%   +0.01%     
==========================================
  Files         897      898       +1     
  Lines       67627    67652      +25     
  Branches     2662     2663       +1     
==========================================
+ Hits        53688    53721      +33     
+ Misses      13266    13263       -3     
+ Partials      673      668       -5     
Flag Coverage Δ
frontend 66.42% <100.00%> (-0.05%) ⬇️
python 79.42% <ø> (ø)
ruby-api 82.44% <100.00%> (+0.19%) ⬆️
ruby-backend 84.77% <100.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Share plugin venv resolution and environment setup across script execution,
suite analysis, and microservice launch paths.
Comment thread openc3/lib/openc3/utilities/python_venv.rb Fixed
Verify command timestamps fall within the command execution window instead
of relying on a fixed timing tolerance.
@mcosgriff mcosgriff self-assigned this Aug 25, 2026
@mcosgriff
mcosgriff marked this pull request as ready for review August 28, 2026 21:37
calmonroe
calmonroe previously approved these changes Sep 2, 2026
@@ -159,6 +160,15 @@ def self.process_suite(name, contents, new_process: true, username: nil, scope:)
# Preserve PYTHONPATH to ensure Python can find both UV venv and user packages
process.environment['PYTHONPATH'] = ENV['PYTHONPATH'] || '.'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You're setting all these values here but then clobbering them in the configure_for_script call.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wait the configure_for_script doesn't always override ... only if the venv resolves. Targets without a a venv need this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, only clobbers when the venv resolves. The one real overlap was PYTHONPATH - configure_environment read ENV instead of the value seeded here. Fixed in the other thread.

environment['PYTHONUSERBASE'] = venv_dir

site_packages = Dir.glob("#{venv_dir}/lib/python*/site-packages").first
existing_pythonpath = ENV.fetch('PYTHONPATH', '')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you need to prefer a seeded PYTHONPATH being dropped into this method:

existing_pythonpath = environment['PYTHONPATH'] || ENV.fetch('PYTHONPATH', '')

Script.process_suite is already setting PYTHONPATH to the ENV var or '.' (not blank).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Now environment['PYTHONPATH'] || ENV.fetch('PYTHONPATH', '').

unless python_venv_dir
system_venv = File.dirname(ENV.fetch('OPENC3_PYTHON_BIN', '/openc3/python/.venv/bin/python')).chomp('/bin')
process.environment['VIRTUAL_ENV'] = system_venv
process.environment['PYTHONUSERBASE'] = ENV.fetch('PYTHONUSERBASE', nil)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of nil I think the default should be /gems/python_packages to match script.rb, microservice_operator.rb and microservice_model.rb

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Honestly this is probably worth a constant somewhere since it's in 4 different files

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Default is now PythonVenv::DEFAULT_PYTHONUSERBASE, used in script.rb, running_script.rb, microservice_operator.rb and microservice_model.rb.

system_venv = File.dirname(ENV.fetch('OPENC3_PYTHON_BIN', '/openc3/python/.venv/bin/python')).chomp('/bin')
process.environment['VIRTUAL_ENV'] = system_venv
process.environment['PYTHONUSERBASE'] = ENV.fetch('PYTHONUSERBASE', nil)
process.environment['PYTHONPATH'] = ENV.fetch('PYTHONPATH', nil)

@jmthomas jmthomas Sep 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Worth a comment to note that this is explicitly nil so it does not override with a bad path like .

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a comment noting the nil is deliberate so it can't default to . and let a local file shadow a real module.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This isn't filtering by scope. Every other method takes the scope as a parameter. Change the Dir.glob to filter by scope. Users in different scopes shouldn't see python venvs in other scopes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Takes scope now and globs <scope>__*/. Also added the same check in resolve_script_venv - a temp script could pass pythonVenv: "OTHER__demo" directly and File.basename only stripped traversal, not cross-scope.

@jmthomas

jmthomas commented Sep 3, 2026

Copy link
Copy Markdown
Member

Claude created the writeup. The TLDR is that we're prepending the plugins venv before the openc3 package. Thus if a plugin includes openc3 as a dependency they'll get an old version. We don't do this in our DEMO but others might cuz it sort of makes sense to have openc3 as a dependency.

configure_environment builds: environment['PYTHONPATH'] = "#{site_packages}:#{existing_pythonpath}"

Plugin site-packages goes first. In the container existing_pythonpath starts with /openc3/python/.venv/lib/pythonX/site-packages (openc3-ruby/Dockerfile:168), so the plugin venv sits ahead of openc3's own.

The prepend has to exist: uvinstall creates plugin venvs with plain uv venv — no --system-site-packages — and the process still runs /openc3/python/.venv/bin/python. Splicing PYTHONPATH is the only way plugin packages reach the script.

The risk: any plugin venv that contains openc3 shadows the container's copy. A plugin pinned to an older openc3 silently downgrades the library talking to your current API — no error, wrong behavior wherever the API changed.

What I found just now that softens it: the demo plugin already guards against this by convention. openc3-cosmos-demo/pyproject.toml puts openc3 in [dependency-groups] dev with the comment "Only needed for development/testing; provided by COSMOS at runtime", and uvinstall runs uv sync --frozen --no-dev --no-install-project — so the dev group is excluded. The reference plugin is safe, and the flags back the convention up.

What's still open: nothing enforces it. A third-party plugin that lists openc3 under [project] dependencies instead of the dev group gets it installed, and it shadows, with no warning at install or run time. So the question is whether that's an acceptable documented convention or wants a guardrail:

  • append plugin site-packages after openc3's rather than prepending — a plugin then can't override anything openc3 ships, which is arguably what you want anyway
  • have uvinstall drop openc3 from the venv after sync
  • warn at plugin install when the resolved venv contains openc3

Same block, same root cause, separate decisions: VIRTUAL_ENV and PATH are pointed at a venv whose interpreter never runs and which has no openc3 in it, so anything downstream honoring either (uv run, a bare python subprocess) gets an interpreter that can't import openc3.

@mcosgriff

Copy link
Copy Markdown
Contributor Author

Claude created the writeup. The TLDR is that we're prepending the plugins venv before the openc3 package. Thus if a plugin includes openc3 as a dependency they'll get an old version. We don't do this in our DEMO but others might cuz it sort of makes sense to have openc3 as a dependency.

configure_environment builds: environment['PYTHONPATH'] = "#{site_packages}:#{existing_pythonpath}"

Plugin site-packages goes first. In the container existing_pythonpath starts with /openc3/python/.venv/lib/pythonX/site-packages (openc3-ruby/Dockerfile:168), so the plugin venv sits ahead of openc3's own.

The prepend has to exist: uvinstall creates plugin venvs with plain uv venv — no --system-site-packages — and the process still runs /openc3/python/.venv/bin/python. Splicing PYTHONPATH is the only way plugin packages reach the script.

The risk: any plugin venv that contains openc3 shadows the container's copy. A plugin pinned to an older openc3 silently downgrades the library talking to your current API — no error, wrong behavior wherever the API changed.

What I found just now that softens it: the demo plugin already guards against this by convention. openc3-cosmos-demo/pyproject.toml puts openc3 in [dependency-groups] dev with the comment "Only needed for development/testing; provided by COSMOS at runtime", and uvinstall runs uv sync --frozen --no-dev --no-install-project — so the dev group is excluded. The reference plugin is safe, and the flags back the convention up.

What's still open: nothing enforces it. A third-party plugin that lists openc3 under [project] dependencies instead of the dev group gets it installed, and it shadows, with no warning at install or run time. So the question is whether that's an acceptable documented convention or wants a guardrail:

  • append plugin site-packages after openc3's rather than prepending — a plugin then can't override anything openc3 ships, which is arguably what you want anyway
  • have uvinstall drop openc3 from the venv after sync
  • warn at plugin install when the resolved venv contains openc3

Same block, same root cause, separate decisions: VIRTUAL_ENV and PATH are pointed at a venv whose interpreter never runs and which has no openc3 in it, so anything downstream honoring either (uv run, a bare python subprocess) gets an interpreter that can't import openc3.

I feel like we should protect against openc3 being in the dependency list -- maybe a step in uvinstall which will fail the install and tell the user why?

- Confine plugin venvs to the caller's scope: plugin_python_venvs now
  globs <scope>__*/ and resolve_script_venv rejects a temp script
  venv from another scope, which File.basename did not catch
- Prefer a PYTHONPATH already seeded on environment in
  configure_environment so Script.process_suite's value is not lost
- Default PYTHONUSERBASE to PythonVenv::DEFAULT_PYTHONUSERBASE instead
  of nil, replacing the literal in the four files that repeated it
- Use PythonVenv.plugin_venv_path in MicroserviceModel#runtime_python_env
  rather than rebuilding the sanitized path by hand

Refs #3732

Co-Authored-By: Claude noreply@anthropic.com
Comment thread openc3-cosmos-script-runner-api/app/controllers/scripts_controller.rb Dismissed
Comment thread openc3/lib/openc3/utilities/python_venv.rb Dismissed
Apply the same tr() PluginModel.plugin_venv_name builds venv directory
fix(python-venv): whitelist temp script venv name characters

Apply the same tr() PluginModel.plugin_venv_name builds venv directory
names with, so glob metacharacters cannot survive into the Dir.glob in
configure_environment. File.basename and the scope prefix check both
pass a name like "DEFAULT__*"; only File.directory? stopped it, which
made the guarantee depend on filesystem state rather than the value.

Refs #3732

Co-Authored-By: Claude noreply@anthropic.com
@sonarqubecloud

sonarqubecloud Bot commented Sep 9, 2026

Copy link
Copy Markdown

@mcosgriff
mcosgriff requested a review from jmthomas September 9, 2026 17:09
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.

Python script suite analysis does not use venv?

4 participants