From 1d78914631f862355d217bb94a842030275e70a2 Mon Sep 17 00:00:00 2001
From: Tim Pillinger <26465611+wxtim@users.noreply.github.com>
Date: Thu, 21 Sep 2023 16:26:06 +0100
Subject: [PATCH] Remove PYTHONPATH items from sys.path Add CYLC_PYTHONPATH
items to sys.path
---
changes.d/5727.break.md | 2 ++
.../etc/python-job.settings | 4 +--
cylc/flow/scripts/cylc.py | 29 +++++++++++++++++--
tests/unit/scripts/test_cylc.py | 28 +++++++++++++++++-
4 files changed, 58 insertions(+), 5 deletions(-)
create mode 100644 changes.d/5727.break.md
diff --git a/changes.d/5727.break.md b/changes.d/5727.break.md
new file mode 100644
index 00000000000..8d0a1cd2314
--- /dev/null
+++ b/changes.d/5727.break.md
@@ -0,0 +1,2 @@
+Cylc now ignores `PYTHONPATH` to make it more robust to task environments which set this value.
+If you want to add to the Cylc environment itself, e.g. to install a Cylc extension, use `CYLC_PYTHONPATH`.
\ No newline at end of file
diff --git a/cylc/flow/etc/tutorial/cylc-forecasting-workflow/etc/python-job.settings b/cylc/flow/etc/tutorial/cylc-forecasting-workflow/etc/python-job.settings
index 15de1f8ea13..a1366917d8f 100644
--- a/cylc/flow/etc/tutorial/cylc-forecasting-workflow/etc/python-job.settings
+++ b/cylc/flow/etc/tutorial/cylc-forecasting-workflow/etc/python-job.settings
@@ -7,6 +7,6 @@
[[[environment]]]
# These environment variables ensure that tasks can
# run in the same environment as the workflow:
- {% from "sys" import path, executable %}
- PYTHONPATH = {{':'.join(path)}}
+ {% from "sys" import executable %}
+ PYTHONPATH = {{executable}}/../lib/python:$PYTHONPATH
PATH = $(dirname {{executable}}):$PATH
diff --git a/cylc/flow/scripts/cylc.py b/cylc/flow/scripts/cylc.py
index 4f13dc59496..c65b4c8c9ba 100644
--- a/cylc/flow/scripts/cylc.py
+++ b/cylc/flow/scripts/cylc.py
@@ -16,10 +16,35 @@
# along with this program. If not, see .
"""cylc main entry point"""
-import argparse
-from contextlib import contextmanager
import os
import sys
+
+
+def pythonpath_manip():
+ """Stop PYTHONPATH contaminating the Cylc Environment
+
+ * Remove PYTHONPATH items from sys.path to prevent PYTHONPATH
+ contaminating the Cylc Environment.
+ * Re-add items from CYLC_PYTHONPATH to sys.path.
+
+ See Also:
+ https://github.com/cylc/cylc-flow/issues/5124
+ """
+ if 'CYLC_PYTHONPATH' in os.environ:
+ for item in os.environ['CYLC_PYTHONPATH'].split(os.pathsep):
+ abspath = os.path.abspath(item)
+ sys.path.insert(0, abspath)
+ elif 'PYTHONPATH' in os.environ:
+ for item in os.environ['PYTHONPATH'].split(os.pathsep):
+ abspath = os.path.abspath(item)
+ if abspath in sys.path:
+ sys.path.remove(abspath)
+
+
+pythonpath_manip()
+
+import argparse
+from contextlib import contextmanager
from typing import Iterator, NoReturn, Optional, Tuple
from ansimarkup import parse as cparse
diff --git a/tests/unit/scripts/test_cylc.py b/tests/unit/scripts/test_cylc.py
index f1483e9ee4a..4656aeacb52 100644
--- a/tests/unit/scripts/test_cylc.py
+++ b/tests/unit/scripts/test_cylc.py
@@ -15,14 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+import os
import pkg_resources
+import sys
from types import SimpleNamespace
from typing import Callable
from unittest.mock import Mock
import pytest
-from cylc.flow.scripts.cylc import iter_commands
+from cylc.flow.scripts.cylc import iter_commands, pythonpath_manip
from ..conftest import MonkeyMock
@@ -136,3 +138,27 @@ def test_execute_cmd(
# the "bad" entry point should raise an exception
with pytest.raises(ModuleNotFoundError):
execute_cmd('bad')
+
+
+def test_pythonpath_manip(monkeypatch):
+ """pythonpath_manip removes items in PYTHONPATH from sys.path
+
+ and adds items from CYLC_PYTHONPATH
+ """
+ # If PYTHONPATH is set...
+ monkeypatch.setenv('PYTHONPATH', '/remove-from-sys.path')
+ monkeypatch.setattr('sys.path', ['/leave-alone', '/remove-from-sys.path'])
+ pythonpath_manip()
+ # ... we don't change PYTHONPATH
+ assert os.environ['PYTHONPATH'] == '/remove-from-sys.path'
+ # ... but we do remove PYTHONPATH items from sys.path, and don't remove
+ # items there not in PYTHONPATH
+ assert sys.path == ['/leave-alone']
+ # ... and store items from PYTHONPATH in CYLC_PYTHONPATH
+ assert os.environ['CYLC_PYTHONPATH'] == '/remove-from-sys.path'
+
+ # If CYLC_PYTHONPATH is set we retrieve its contents and
+ # add them to the sys.path:
+ monkeypatch.setenv('CYLC_PYTHONPATH', '/add-to-sys.path')
+ pythonpath_manip()
+ assert sys.path == ['/add-to-sys.path', '/leave-alone']