Repository: gabrieldemarmiesse/python-on-whales (language: python)

Issue:
ComposeConfigService should include entrypoint
It seems necessary/useful to represent how a service is invoked by including both the entrypoint as well as the command.

Reading components.compose.models, I see:
```
class ComposeConfigService(BaseModel):
    deploy: Optional[ServiceDeployConfig]
    blkio_config: Any
    cpu_count: Optional[float]
    cpu_percent: Optional[float]
    cpu_shares: Optional[int]
    cpuset: Optional[str]
    build: Any
    cap_add: List[str] = Field(default_factory=list)
    cap_drop: List[str] = Field(default_factory=list)
    cgroup_parent: Optional[str]
    command: Optional[List[str]]
    configs: Any
    container_name: Optional[str]
    depends_on: Dict[str, DependencyCondition] = Field(default_factory=dict)
    device_cgroup_rules: List[str] = Field(default_factory=list)
    devices: Any
    environment: Optional[Dict[str, Optional[str]]]
    image: Optional[str]
```
command is there, but no entrypoint. entrypoint is included in the json output from compose cli but not picked up by this model (info is lost).

In the meantime, for my use case it was imperative that I read the entrypoint specified by the compose file. I've monkeypatched my code with this file:
monkeypatched_whales.py:
```
from typing import Optional, Any, List, Dict
from pydantic import BaseModel, Field
from python_on_whales.components.compose import models
from python_on_whales.components.compose.models import ServiceDeployConfig, DependencyCondition, \
    ComposeConfigNetwork, ComposeConfigVolume


class ComposeConfigServiceWithEntrypoint(BaseModel):
    deploy: Optional[ServiceDeployConfig]
    blkio_config: Any
    cpu_count: Optional[float]
    cpu_percent: Optional[float]
    cpu_shares: Optional[int]
    cpuset: Optional[str]
    build: Any
    cap_add: List[str] = Field(default_factory=list)
    cap_drop: List[str] = Field(default_factory=list)
    cgroup_parent: Optional[str]
    command: Optional[List[str]]
    configs: Any
    container_name: Optional[str]
    depends_on: Dict[str, DependencyCondition] = Field(default_factory=dict)
    device_cgroup_rules: List[str] = Field(default_factory=list)
    devices: Any
    entrypoint: Optional[List[str]]
    environment: Optional[Dict[str, Optional[str]]]
    image: Optional[str]


class ComposeConfigWithEntrypoint(BaseModel):
    services: Dict[str, ComposeConfigServiceWithEntrypoint]
    networks: Dict[str, ComposeConfigNetwork] = Field(default_factory=dict)
    volumes: Dict[str, ComposeConfigVolume] = Field(default_factory=dict)
    configs: Any
    secrets: Any

models.ComposeConfigService = ComposeConfigServiceWithEntrypoint
models.ComposeConfig = ComposeConfigWithEntrypoint
from python_on_whales.components.compose import cli_wrapper

cli_wrapper.ComposeConfig = ComposeConfigWithEntrypoint
```

I'm aware that it'll be a cat-and-mouse game keeping up with the compose spec but this particular case seems like an oversight.



Discussion / hints:
Also note, compose cli issue progress for including entrypoint when it is an empty list: #https://github.com/docker/compose-cli/issues/1922
Hello, thank you for the bug report!

I'm fine with this cat and mouse game as it improves greatly the dev expérience when it works, and when it doesn't it's quite fast to fix.

I'll make a pull request. If you see something missing from a model next time, feel free to open a pull request, i'll gladly review it :)

If you're not familiar with open source, check out https://github.com/gabrieldemarmiesse/getting_started_open_source :)

I'll make the pull request this morning and a release this afternoon most likely. 

Fix patch:
diff --git a/python_on_whales/components/compose/models.py b/python_on_whales/components/compose/models.py
index 34609a14..979a80b7 100644
--- a/python_on_whales/components/compose/models.py
+++ b/python_on_whales/components/compose/models.py
@@ -36,6 +36,7 @@ class ComposeConfigService(BaseModel):
     device_cgroup_rules: List[str] = Field(default_factory=list)
     devices: Any
     environment: Dict[str, Optional[str]]
+    entrypoint: List[str]
     image: str
 
 


Test patch:
diff --git a/tests/python_on_whales/components/dummy_compose.yml b/tests/python_on_whales/components/dummy_compose.yml
index 75a8ad51..a58eab86 100644
--- a/tests/python_on_whales/components/dummy_compose.yml
+++ b/tests/python_on_whales/components/dummy_compose.yml
@@ -21,3 +21,6 @@ services:
     environment:
      - DD_API_KEY=__your_datadog_api_key_here__
      - POSTGRES_HOST_AUTH_METHOD=trust
+  dodo:
+    image: busybox:latest
+    entrypoint: /bin/dudu
diff --git a/tests/python_on_whales/components/my_service_build/Dockerfile b/tests/python_on_whales/components/my_service_build/Dockerfile
index 75ffc3f7..445932d1 100644
--- a/tests/python_on_whales/components/my_service_build/Dockerfile
+++ b/tests/python_on_whales/components/my_service_build/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.7
+FROM python:3.7-slim
 
 RUN pip install redis
 
diff --git a/tests/python_on_whales/components/test_compose.py b/tests/python_on_whales/components/test_compose.py
index 06cc16f5..d74031fa 100644
--- a/tests/python_on_whales/components/test_compose.py
+++ b/tests/python_on_whales/components/test_compose.py
@@ -4,7 +4,8 @@
 import pytest
 
 import python_on_whales
-from python_on_whales import DockerClient, DockerException
+from python_on_whales import DockerClient
+from python_on_whales.exceptions import NoSuchImage
 from python_on_whales.utils import PROJECT_ROOT
 
 pytestmark = pytest.mark.skipif(
@@ -39,12 +40,12 @@ def test_no_containers():
 
 
 def test_docker_compose_up_detach_down():
-    docker.compose.up(detach=True)
+    docker.compose.up(["my_service", "busybox", "alpine"], detach=True)
     docker.compose.down()
 
 
 def test_docker_compose_pause_unpause():
-    docker.compose.up(detach=True)
+    docker.compose.up(["my_service", "busybox", "alpine"], detach=True)
     docker.compose.pause()
     for container in docker.compose.ps():
         assert container.state.paused
@@ -82,24 +83,24 @@ def test_docker_compose_create_extra_options_down():
 
 
 def test_docker_compose_up_detach_down_extra_options():
-    docker.compose.up(detach=True)
+    docker.compose.up(["my_service", "busybox", "alpine"], detach=True)
     docker.compose.down(remove_orphans=True, remove_images="all", timeout=3)
 
 
 def test_docker_compose_up_build():
-    docker.compose.up(build=True, detach=True)
+    docker.compose.up(["my_service", "busybox", "alpine"], build=True, detach=True)
     with docker.image.inspect("some_random_image"):
         docker.compose.down()
 
 
 def test_docker_compose_up_stop_rm():
-    docker.compose.up(build=True, detach=True)
+    docker.compose.up(["my_service", "busybox", "alpine"], build=True, detach=True)
     docker.compose.stop(timeout=timedelta(seconds=3))
     docker.compose.rm(volumes=True)
 
 
 def test_docker_compose_up_rm():
-    docker.compose.up(build=True, detach=True)
+    docker.compose.up(["my_service", "busybox", "alpine"], build=True, detach=True)
     docker.compose.rm(stop=True, volumes=True)
 
 
@@ -137,11 +138,11 @@ def test_docker_compose_kill():
 def test_docker_compose_pull():
     try:
         docker.image.remove("busybox")
-    except DockerException:
+    except NoSuchImage:
         pass
     try:
         docker.image.remove("alpine")
-    except DockerException:
+    except NoSuchImage:
         pass
     docker.compose.pull(["busybox", "alpine"])
     docker.image.inspect(["busybox", "alpine"])
@@ -173,3 +174,7 @@ def test_passing_env_files(tmp_path: Path):
     output = docker.compose.config()
 
     assert output.services["alpine"].environment["SOME_VARIABLE"] == "hello"
+
+
+def test_entrypoint_loaded_in_config():
+    assert docker.compose.config().services["dodo"].entrypoint == ["/bin/dudu"]