From f40b58f223c98f0c499b81d6d14a40a345e9b3f6 Mon Sep 17 00:00:00 2001 From: Pontus Lurcock Date: Fri, 19 Dec 2025 14:17:25 +0100 Subject: [PATCH 1/2] Look for environment.yml automatically Addresses #41 --- test/test_core.py | 35 +++++++++++++++++++++++++---------- xcengine/core.py | 16 ++++++++++++---- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/test/test_core.py b/test/test_core.py index a53b953..486b26e 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -29,27 +29,35 @@ @patch("xcengine.core.ScriptCreator.__init__") @pytest.mark.parametrize("tag", [None, "bar"]) -@pytest.mark.parametrize("use_env", [False, True]) -def test_image_builder_init(init_mock, tmp_path, tag, use_env): +@pytest.mark.parametrize("env_file_name", ["environment.yml", "foo.yaml", None]) +@pytest.mark.parametrize("use_env_file_param", [False, True]) +def test_image_builder_init( + init_mock, + tmp_path: pathlib.Path, + tag: str | None, + env_file_name: str | None, + use_env_file_param: bool, +): nb_path = tmp_path / "foo.ipynb" nb_path.touch() - if use_env: - environment = tmp_path / "environment.yml" - environment.touch() + if env_file_name is not None: + environment_path = tmp_path / env_file_name + environment_path.touch() else: - environment = None + environment_path = None build_path = tmp_path / "build" build_path.mkdir() init_mock.return_value = None ib = ImageBuilder( notebook=nb_path, - environment=environment, + environment=environment_path if use_env_file_param else None, build_dir=build_path, tag=tag, ) assert ib.notebook == nb_path assert ib.build_dir == build_path - assert ib.environment == environment + expected_env = environment_path if (use_env_file_param or env_file_name == "environment.yml") else None + assert ib.environment == expected_env if tag is None: assert abs( datetime.datetime.now(datetime.UTC) @@ -97,12 +105,13 @@ def test_runner_init_with_image(): ) assert runner.image == image + @pytest.mark.parametrize("keep", [False, True]) def test_runner_run_keep(keep: bool): runner = xcengine.core.ContainerRunner( image := Mock(docker.models.images.Image), None, - client := Mock(DockerClient) + client := Mock(DockerClient), ) image.tags = [] client.containers.run.return_value = (container := MagicMock(Container)) @@ -118,26 +127,32 @@ def test_runner_sigint(): runner = xcengine.core.ContainerRunner( image := Mock(docker.models.images.Image), None, - client := Mock(DockerClient) + client := Mock(DockerClient), ) image.tags = [] client.containers.run.return_value = (container := Mock(Container)) container.status = "running" + def container_stop(): container.status = "stopped" + container.stop = container_stop pid = os.getpid() old_alarm_handler = signal.getsignal(signal.SIGALRM) + class AlarmException(Exception): pass + def alarm_handler(signum, frame): raise AlarmException() + signal.signal(signal.SIGALRM, alarm_handler) def interrupt_process(): time.sleep(1) # allow one second for runner to start os.kill(pid, signal.SIGINT) + thread = threading.Thread(target=interrupt_process, daemon=True) thread.start() diff --git a/xcengine/core.py b/xcengine/core.py index 1efbf95..73a1d85 100755 --- a/xcengine/core.py +++ b/xcengine/core.py @@ -179,6 +179,7 @@ class ImageBuilder: """ tag_format: ClassVar[str] = "%Y.%m.%d.%H.%M.%S" + environment: pathlib.Path | None = None def __init__( self, @@ -206,7 +207,9 @@ def __init__( else: self.tag = tag - if environment is None: + if environment is not None: + self.environment = environment + else: LOGGER.info( "Looking for environment file configuration in the notebook." ) @@ -218,9 +221,14 @@ def __init__( self.environment = notebook.parent / nb_env else: LOGGER.info(f"No environment specified in notebook.") - self.environment = None - else: - self.environment = environment + LOGGER.info(f"Looking for a file named \"environment.yml\".") + notebook_sibling = notebook.parent / "environment.yml" + if notebook_sibling.is_file(): + self.environment = notebook_sibling + LOGGER.info(f"Using environment from {notebook_sibling}") + else: + LOGGER.info(f"No environment found at {notebook_sibling}") + self.environment = None def build(self) -> Image: self.script_creator.convert_notebook_to_script(self.build_dir) From ad1453406ec39e18047e03957619a988975e5926 Mon Sep 17 00:00:00 2001 From: Pontus Lurcock Date: Fri, 19 Dec 2025 14:18:51 +0100 Subject: [PATCH 2/2] Update changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 12c2889..7444b2e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ * Improve documentation (#54, #55) * Improve type annotations and checks (#68) * Include Dockerfile in built images (#55) +* Look for environment.yml automatically (#41) ## Changes in 0.1.1