Repository: python-poetry/poetry

Issue:
command to check lock file
<!-- Checked checkbox should look like this: [x] -->
- [X] I have searched the [issues](https://github.com/sdispater/poetry/issues) of this repo and believe that this is not a duplicate.

## Issue

Would it be possible to add a `poetry check` command (oh, I see you'e used that already, maybe the command could be extended or have a `poetry lock --check`?) that will return zero if the current poetry.lock is both correct w.r.t. the pyproject.toml (ie: packages match) *and* that it matches the output of `poetry lock`.



Discussion:
It's very useful in CI/CD. With this command we could check that programmer didn't forget re-lock requirements.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

This feature request is still valid.
I agree that this would be a nice feature!
This feature is available in some other package managers. I list some examples below:  

- `npm ci`: https://docs.npmjs.com/cli/ci.html
- `yarn install --frozen-lockfile`: https://yarnpkg.com/lang/en/docs/cli/install/#toc-yarn-install-frozen-lockfile
I had figure out script to emulate the behavior of #1954 and fortunately it is simple:
```shell
python -c "from poetry.factory import Factory; l = Factory().create_poetry('.').locker; exit(0) if l.is_locked() and l.is_fresh() else exit(1)"  && echo 'OK'
```
worth mentioning that you need poetry on your `PYTHONPATH` which you can do like:
```bash
PYTHONPATH="${PYTHONPATH}:${HOME}/.poetry/lib" \
python -c "from poetry.factory import Factory; l = Factory().create_poetry('.').locker; exit(0) if l.is_locked() and l.is_fresh() else exit(1)"  && echo 'OK'
``` 
Thank you both!

I think it's also worth mentioning if you have not followed the recommended install, the necessary changes to PYTHONPATH might differ.

In my case, on ArchLinux, where I installed poetry via `pacman`, I had to:
```
PYTHONPATH="${PYTHONPATH}:/usr/lib/python3.8/site-packages"
poetry run python -c "from poetry.factory import Factory; l = Factory().create_poetry('.').locker; exit(0) if l.is_locked() and l.is_fresh() else exit(1)" && echo 'OK'
```
I'll add to my requirements wishlist for this: also check that the .lock file is consistent with what's actually in the managed python environment.

I've commented on @cliebBS pull request #1954, but thought I'd mention it here too for completeness.
I'd like to report back and say this check in it's current form isn't really useful for me… it only checks to make sure that `poetry update` would result in no updates.

I thought it would function more like [bundlers deployment mode](https://bundler.io/man/bundle-install.1.html#DEPLOYMENT-MODE). So that it errors if the pyproject.toml has been modified but the lock file is out of date.
If you use `poetry-dynamic-versioning` and the above mentioned script is failing, you can try this one:
```
PYTHONPATH="${PYTHONPATH}:${HOME}/.poetry/lib:${HOME}/.poetry/lib/poetry/_vendor/py3.6/" python -c "from poetry.factory import Factory; from pathlib import Path; l = Factory().create_poetry(Path('.')).locker; exit(0) if l.is_locked() and l.is_fresh() else exit(1)"  && echo 'OK' || (echo 'Lock file out of date. Maybe you need to do "poetry lock"'; exit 1)
```
Another couple of ideas what can be done in CI scripts:

Generate `requirements.txt` before and after updating the lock:
```
poetry export -f requirements.txt --output requirements-expected.txt
poetry lock --no-update
poetry export -f requirements.txt --output requirements-actual.txt
if ! diff --ignore-all-space "requirements-expected.txt" "requirements-actual.txt"; then
  echo "poetry.lock file is outdated, please run 'poetry lock' and commit updated poetry.lock"
  exit 1
fi
```
(additional flags could be used: `poetry --dev --without-hashes export …`).

Or slightly simpler approach (but less reliable) is to generate lock file and check if differences were found in it using git:
```
poetry lock --no-update
if ! git diff --exit-code; then
  echo "poetry.lock file is outdated, please run 'poetry lock' and commit updated poetry.lock"
  exit 1
fi
```
The problem is that `poetry update` does two things, if my understanding is right:

1. Updates/installs/removes packages to match changes that were made to pyproject.toml

2. Updates packages even their specification did **not change** in pyproject.toml wasn’t because it finds a new version which is allowed by semver

In the first case, the `peotry.lock` does no longer match the `pyproject.toml`, it's "illegal". 
In the second case, the `poetry.lock` still matches the the `pyproject.toml`, it's legal, just not using the latest semver-allowed version.

In CI, you usually want to force a `poetry update` in the first case, but not the second. 

The proposed solutions in this thread do not differentiate between these two cases as far as I could tell. Therefore they aren't really a solution and we still need a command.


Resolving patch:
diff --git a/poetry/console/commands/lock.py b/poetry/console/commands/lock.py
--- a/poetry/console/commands/lock.py
+++ b/poetry/console/commands/lock.py
@@ -12,6 +12,12 @@ class LockCommand(InstallerCommand):
         option(
             "no-update", None, "Do not update locked versions, only refresh lock file."
         ),
+        option(
+            "check",
+            None,
+            "Check that the <comment>poetry.lock</> file corresponds to the current version "
+            "of <comment>pyproject.toml</>.",
+        ),
     ]
 
     help = """
@@ -29,6 +35,13 @@ def handle(self) -> int:
             self.poetry.config.get("experimental.new-installer", False)
         )
 
+        if self.option("check"):
+            return (
+                0
+                if self.poetry.locker.is_locked() and self.poetry.locker.is_fresh()
+                else 1
+            )
+
         self._installer.lock(update=not self.option("no-update"))
 
         return self._installer.run()


Test patch:
diff --git a/tests/console/commands/test_lock.py b/tests/console/commands/test_lock.py
--- a/tests/console/commands/test_lock.py
+++ b/tests/console/commands/test_lock.py
@@ -16,9 +16,8 @@ def tester(command_tester_factory):
     return command_tester_factory("lock")
 
 
-@pytest.fixture
-def poetry_with_old_lockfile(project_factory, fixture_dir, source_dir):
-    source = fixture_dir("old_lock")
+def _project_factory(fixture_name, project_factory, fixture_dir):
+    source = fixture_dir(fixture_name)
     pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8")
     poetry_lock_content = (source / "poetry.lock").read_text(encoding="utf-8")
     return project_factory(
@@ -28,6 +27,57 @@ def poetry_with_old_lockfile(project_factory, fixture_dir, source_dir):
     )
 
 
+@pytest.fixture
+def poetry_with_outdated_lockfile(project_factory, fixture_dir):
+    return _project_factory("outdated_lock", project_factory, fixture_dir)
+
+
+@pytest.fixture
+def poetry_with_up_to_date_lockfile(project_factory, fixture_dir):
+    return _project_factory("up_to_date_lock", project_factory, fixture_dir)
+
+
+@pytest.fixture
+def poetry_with_old_lockfile(project_factory, fixture_dir):
+    return _project_factory("old_lock", project_factory, fixture_dir)
+
+
+def test_lock_check_outdated(
+    command_tester_factory, poetry_with_outdated_lockfile, http
+):
+    http.disable()
+
+    locker = Locker(
+        lock=poetry_with_outdated_lockfile.pyproject.file.path.parent / "poetry.lock",
+        local_config=poetry_with_outdated_lockfile.locker._local_config,
+    )
+    poetry_with_outdated_lockfile.set_locker(locker)
+
+    tester = command_tester_factory("lock", poetry=poetry_with_outdated_lockfile)
+    status_code = tester.execute("--check")
+
+    # exit with an error
+    assert status_code == 1
+
+
+def test_lock_check_up_to_date(
+    command_tester_factory, poetry_with_up_to_date_lockfile, http
+):
+    http.disable()
+
+    locker = Locker(
+        lock=poetry_with_up_to_date_lockfile.pyproject.file.path.parent / "poetry.lock",
+        local_config=poetry_with_up_to_date_lockfile.locker._local_config,
+    )
+    poetry_with_up_to_date_lockfile.set_locker(locker)
+
+    tester = command_tester_factory("lock", poetry=poetry_with_up_to_date_lockfile)
+    status_code = tester.execute("--check")
+
+    # exit with an error
+    assert status_code == 0
+
+
 def test_lock_no_update(command_tester_factory, poetry_with_old_lockfile, repo):
     repo.add_package(get_package("sampleproject", "1.3.1"))
     repo.add_package(get_package("sampleproject", "2.0.0"))
diff --git a/tests/fixtures/outdated_lock/poetry.lock b/tests/fixtures/outdated_lock/poetry.lock
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/outdated_lock/poetry.lock
@@ -0,0 +1,152 @@
+[[package]]
+name = "certifi"
+version = "2020.6.20"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "chardet"
+version = "3.0.4"
+description = "Universal encoding detector for Python 2 and 3"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "docker"
+version = "4.3.0"
+description = "A Python library for the Docker Engine API."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+pywin32 = {version = "227", markers = "sys_platform == \"win32\""}
+requests = ">=2.14.2,<2.18.0 || >2.18.0"
+six = ">=1.4.0"
+websocket-client = ">=0.32.0"
+
+[package.extras]
+ssh = ["paramiko (>=2.4.2)"]
+tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"]
+
+[[package]]
+name = "idna"
+version = "2.10"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pywin32"
+version = "227"
+description = "Python for Window Extensions"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "requests"
+version = "2.24.0"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+chardet = ">=3.0.2,<4"
+idna = ">=2.5,<3"
+urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
+
+[package.extras]
+security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
+socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
+
+[[package]]
+name = "six"
+version = "1.15.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "urllib3"
+version = "1.25.10"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+brotli = ["brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
+socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
+
+[[package]]
+name = "websocket-client"
+version = "0.57.0"
+description = "WebSocket client for Python. hybi13 is supported."
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+six = "*"
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.8"
+content-hash = "2f47de5e052dabeff3c1362d3a37b5cfcaf9bbe9d9ce1681207e72ca1f4dab55"
+
+[metadata.files]
+certifi = [
+    {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
+    {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
+]
+chardet = [
+    {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
+    {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
+]
+docker = [
+    {file = "docker-4.3.0-py2.py3-none-any.whl", hash = "sha256:ba118607b0ba6bfc1b236ec32019a355c47b5d012d01d976467d4692ef443929"},
+    {file = "docker-4.3.0.tar.gz", hash = "sha256:431a268f2caf85aa30613f9642da274c62f6ee8bae7d70d968e01529f7d6af93"},
+]
+idna = [
+    {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
+    {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
+]
+pywin32 = [
+    {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"},
+    {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"},
+    {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"},
+    {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"},
+    {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"},
+    {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"},
+    {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"},
+    {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"},
+    {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"},
+    {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"},
+    {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"},
+    {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"},
+]
+requests = [
+    {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
+    {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
+]
+six = [
+    {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
+    {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
+]
+urllib3 = [
+    {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"},
+    {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"},
+]
+websocket-client = [
+    {file = "websocket_client-0.57.0-py2.py3-none-any.whl", hash = "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549"},
+    {file = "websocket_client-0.57.0.tar.gz", hash = "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"},
+]
diff --git a/tests/fixtures/outdated_lock/pyproject.toml b/tests/fixtures/outdated_lock/pyproject.toml
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/outdated_lock/pyproject.toml
@@ -0,0 +1,15 @@
+[tool.poetry]
+name = "foobar"
+version = "0.1.0"
+description = ""
+authors = ["Poetry Developer <developer@python-poetry.org>"]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+docker = "4.3.1"
+
+[tool.poetry.dev-dependencies]
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/fixtures/up_to_date_lock/poetry.lock b/tests/fixtures/up_to_date_lock/poetry.lock
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/up_to_date_lock/poetry.lock
@@ -0,0 +1,152 @@
+[[package]]
+name = "certifi"
+version = "2020.12.5"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "chardet"
+version = "4.0.0"
+description = "Universal encoding detector for Python 2 and 3"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "docker"
+version = "4.3.1"
+description = "A Python library for the Docker Engine API."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+pywin32 = {version = "227", markers = "sys_platform == \"win32\""}
+requests = ">=2.14.2,<2.18.0 || >2.18.0"
+six = ">=1.4.0"
+websocket-client = ">=0.32.0"
+
+[package.extras]
+ssh = ["paramiko (>=2.4.2)"]
+tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"]
+
+[[package]]
+name = "idna"
+version = "2.10"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pywin32"
+version = "227"
+description = "Python for Window Extensions"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "requests"
+version = "2.25.1"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+chardet = ">=3.0.2,<5"
+idna = ">=2.5,<3"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
+socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+
+[[package]]
+name = "six"
+version = "1.15.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "urllib3"
+version = "1.26.3"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+brotli = ["brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "websocket-client"
+version = "0.58.0"
+description = "WebSocket client for Python with low level API options"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+six = "*"
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.8"
+content-hash = "0cd068218f235c162f7b74bc8faf4ce3387b82daee1c1bb7a97af034f27ee116"
+
+[metadata.files]
+certifi = [
+    {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
+    {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
+]
+chardet = [
+    {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
+    {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
+]
+docker = [
+    {file = "docker-4.3.1-py2.py3-none-any.whl", hash = "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828"},
+    {file = "docker-4.3.1.tar.gz", hash = "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2"},
+]
+idna = [
+    {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
+    {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
+]
+pywin32 = [
+    {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"},
+    {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"},
+    {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"},
+    {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"},
+    {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"},
+    {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"},
+    {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"},
+    {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"},
+    {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"},
+    {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"},
+    {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"},
+    {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"},
+]
+requests = [
+    {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
+    {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
+]
+six = [
+    {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
+    {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
+]
+urllib3 = [
+    {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"},
+    {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"},
+]
+websocket-client = [
+    {file = "websocket_client-0.58.0-py2.py3-none-any.whl", hash = "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663"},
+    {file = "websocket_client-0.58.0.tar.gz", hash = "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"},
+]
diff --git a/tests/fixtures/up_to_date_lock/pyproject.toml b/tests/fixtures/up_to_date_lock/pyproject.toml
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/up_to_date_lock/pyproject.toml
@@ -0,0 +1,15 @@
+[tool.poetry]
+name = "foobar"
+version = "0.1.0"
+description = ""
+authors = ["Poetry Developer <developer@python-poetry.org>"]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+docker = "4.3.1"
+
+[tool.poetry.dev-dependencies]
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"