Skip to content
This repository was archived by the owner on Jun 30, 2024. It is now read-only.

Commit 490c003

Browse files
authored
Merge pull request #2000 from bjones1/poetry-fix-fix
Fix: Better document how poetry-fix works; error if it doesn't process BookServer and the RunestoneComponents.
2 parents 7cac256 + 8eedc53 commit 490c003

File tree

4 files changed

+96
-12
lines changed

4 files changed

+96
-12
lines changed

docker/docker_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
#
4646
# Imports and bootstrap
4747
# =====================
48-
# These are listed in the order prescribed by PEP 8, with exceptions noted below.
48+
# These are listed in the order prescribed by `PEP 8 <http://www.python.org/dev/peps/pep-0008/#imports>`_, with exceptions noted below.
4949
#
5050
# There's a fair amount of bootstrap code here to download and install required imports and their dependencies.
5151
#

runestone_poetry_project/poetry_fix.py

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,72 @@
11
# ***********************************
22
# |docname| - Work around Poetry bugs
33
# ***********************************
4-
# This script contains two fixes for Poetry bugs: one bug which manifests when the ``--no-dev`` flag is passed to ``poetry install/update`` and another which occurs when the ``--no-dev`` flag isn't passed. It doesn't provide a fix to a third bug, discussed below
4+
# This script contains workarounds for Poetry design decisions and bugs:
5+
#
6+
# #. Poetry doesn't support either/or dependencies, but this project needs them. Specifically, we want to install either the released, PyPI-published version of the RunestoneComponents and the BookServer, or the development version of these projects which are cloned to the local filesystem. The RunestoneServer ``pyproject.toml`` file therefore contains (with all other dependencies removed for clarity):
7+
#
8+
# .. code-block:: text
9+
#
10+
# [tool.poetry.dependencies]
11+
# bookserver = "^1.0.0"
12+
# runestone = "^6.1.0"
13+
#
14+
# [tool.poetry.dev-dependencies]
15+
# bookserver = { path = "../BookServer", develop = true }
16+
# runestone = { path = "../RunestoneComponents", develop = true }
17+
#
18+
# This breaks Poetry, since it looks for BOTH dependencies during dependency resolution. To work around this, `rename_pyproject <rename_pyproject>` changes this to:
19+
#
20+
# .. code-block:: text
21+
#
22+
# [tool.poetry.dependencies]
23+
# bookserver = "^1.0.0"
24+
# runestone = "^6.1.0"
25+
#
26+
# [tool.no-poetry.dev-dependencies] # <== CHANGED!
27+
# bookserver = { path = "../BookServer", develop = true }
28+
# runestone = { path = "../RunestoneComponents", develop = true }
29+
#
30+
# ...in production mode; it does the opposite (changes ``[tool.poetry.dependencies]`` to ``[tool.no-poetry.dependencies]``) in development mode. This hides the modified section from Poetry, so the file now looks like an either/or project.
31+
#
32+
# #. Poetry doesn't install development dependencies in projects included through a `path dependency <https://python-poetry.org/docs/dependency-specification/#path-dependencies>`_. As a workaround, this script copies development dependencies from a project into an otherwise empty, auto-created "project", but puts them in the production dependencies section of this newly-created "project", so they will be installed. For example, the BookServer ``pyproject.toml`` contains:
33+
#
34+
# .. code-block:: text
35+
#
36+
# [tool.poetry.dev-dependencies]
37+
# black = "~= 22.0"
38+
# console-ctrl = "^0.1.0"
39+
# ...many more, which are omitted for clarity...
40+
#
41+
# Poetry won't install these. Therefore, `make_dev_pyproject <make_dev_pyproject>` creates a "project" named bookserver-dev whose ``pyproject.toml`` contains a copy of the BookServer development dependencies, but placed in the production dependencies section of this ``bookserver-dev`` "project", so they will be installed. For example, the bookserver-dev ``pyproject.toml`` contains:
42+
#
43+
# .. code-block:: text
44+
#
45+
# [tool.poetry.dependencies] # <== CHANGED!
46+
# black = "~= 22.0"
47+
# console-ctrl = "^0.1.0"
48+
# ...many more, which are omitted for clarity...
49+
#
50+
# This also means that the RunestoneServer ``pyproject.toml`` file must be manually edited to include a reference to this "project":
51+
#
52+
# .. code-block:: text
53+
#
54+
# [tool.poetry.dev-dependencies]
55+
# bookserver = { path = "../BookServer", develop = true }
56+
# bookserver-dev = { path = "../bookserver-dev", develop = true } # <== MANUALLY ADDED!
57+
#
58+
# The final result looks like this:
59+
#
60+
# .. image:: poetry_fix_diagram.png
61+
#
62+
# #. Poetry generates invalid package metadata for local path dependencies, so that running ``pip show click`` results in a bunch of exceptions. This program doesn't provide a fix for this bug.
63+
#
64+
# ...and that's how using Poetry makes dependency management easier...
65+
#
566
#
667
# `Invalid package METADATA <https://github.com/python-poetry/poetry/issues/3148>`_
768
# =====================================================================================
8-
# Per this issue, Poetry generates invalid package metadata for local path dependencies. For example, the last few lines of ``.venv/lib/python3.8/site-packages/runestone_poetry_project-0.1.0.dist-info/METADATA`` contain:
69+
# Per the issue linked in the title above, Poetry generates invalid package metadata for local path dependencies (tested on Poetry v1.1.14). For example, the last few lines of ``.venv/lib/python3.8/site-packages/runestone_poetry_project-0.1.0.dist-info/METADATA`` contain:
970
#
1071
# .. code-block:: text
1172
#
@@ -45,7 +106,7 @@
45106
# Requires-Dist: sphinxcontrib-paverutils (>=1.17)
46107
# Requires-Dist: stripe (>=2.0.0,<3.0.0)
47108
#
48-
# ... along with a similar fix to the ``METADATA`` for ``bookserver_dev`` allow ``pip`` to run successfully.
109+
# ... along with a similar fix to the ``METADATA`` for ``bookserver_dev`` allows ``pip`` to run successfully.
49110
#
50111
#
51112
# TODO
@@ -55,11 +116,12 @@
55116
#
56117
# Imports
57118
# =======
58-
# These are listed in the order prescribed by `PEP 8`_.
119+
# These are listed in the order prescribed by `PEP 8 <http://www.python.org/dev/peps/pep-0008/#imports>`_.
59120
#
60121
# Standard library
61122
# ----------------
62123
from pathlib import Path
124+
import sys
63125
from typing import Any, Dict, Set
64126

65127
# Third-party imports
@@ -72,7 +134,6 @@
72134
# -------------------------
73135
# None.
74136
#
75-
#
76137
# Fix for ``dev-dependencies`` in subprojects
77138
# ===========================================
78139
# Given a main Poetry ``pyproject.toml``, these functions look for all subprojects included via path dependencies, creating additional subprojects named ``projectname-dev`` in which the subproject's dev-dependencies become dependencies in the newly-created subproject. This is a workaround for Poetry's inability to install the dev dependencies for a sub project included via a path requirement. To use this, in the main project, do something like:
@@ -124,12 +185,14 @@ def walk_dependencies(
124185
project_path: Path,
125186
# See `walked_paths_set`.
126187
walked_paths_set: Set[Path],
188+
# See `poetry_paths_set`.
189+
poetry_paths_set: Set[Path],
127190
):
128191
key = "dependencies" if is_deps else "dev-dependencies"
129192
for dep in poetry_dict.get(key, {}).values():
130193
pth = dep.get("path", "") if isinstance(dep, dict) else None
131194
if pth:
132-
walk_pyproject(project_path / pth, walked_paths_set)
195+
walk_pyproject(project_path / pth, walked_paths_set, poetry_paths_set)
133196

134197

135198
# Given a ``pyproject.toml``, optionally create a dev dependencies project and walk all requirements with path dependencies.
@@ -138,6 +201,8 @@ def walk_pyproject(
138201
project_path: Path,
139202
# _`walked_paths_set`: a set of Paths already walked.
140203
walked_paths_set: Set[Path],
204+
# _`poetry_paths_set`: a set of Paths that contained a Poetry project. This is a strict subset of walked_paths_set_.
205+
poetry_paths_set: Set[Path],
141206
# True if this is the root ``pyproject.toml`` file -- no dev dependencies will be created for it.
142207
is_root: bool = False,
143208
):
@@ -153,22 +218,41 @@ def walk_pyproject(
153218
d = toml.load(project_path / "pyproject.toml")
154219
except FileNotFoundError:
155220
return
221+
poetry_paths_set.add(project_path)
156222
tp = d["tool"]["poetry"]
157-
walk_dependencies(tp, True, project_path, walked_paths_set)
158-
walk_dependencies(tp, False, project_path, walked_paths_set)
223+
# Search both the dependencies and dev dependencies in this project for path dependencies.
224+
walk_dependencies(tp, True, project_path, walked_paths_set, poetry_paths_set)
225+
walk_dependencies(tp, False, project_path, walked_paths_set, poetry_paths_set)
159226

160227
# (Usually) process this file.
161228
if not is_root:
162229
create_dev_dependencies(project_path)
163230

164231

232+
# .. _make_dev_pyproject:
233+
#
165234
# Core function: run the whole process on the ``pyproject.toml`` in the current directory.
166235
def make_dev_pyproject():
167-
walk_pyproject(Path("."), set(), True)
236+
project_paths_set = set()
237+
walk_pyproject(Path("."), set(), project_paths_set, True)
238+
239+
# Check that we processed the BookServer and the RunestoneComponents.
240+
found_bookserver = False
241+
found_runestone_components = False
242+
for path in project_paths_set:
243+
name = path.name
244+
found_bookserver |= name == "BookServer"
245+
found_runestone_components |= name == "RunestoneComponents"
246+
if not found_bookserver:
247+
sys.exit("Error: did not process the BookServer Poetry project.")
248+
if not found_runestone_components:
249+
sys.exit("Error: did not process the RunestoneComponents Poetry project.")
168250

169251

170-
# Fix for the main ``pyproject.toml``
171-
# ===================================
252+
# .. _rename_pyproject:
253+
#
254+
# Workaround for the main ``pyproject.toml``
255+
# ==========================================
172256
# This function updates the ``pyproject.toml`` in the current directory by switching between a section named ``[tool.poetry.dev-dependencies]`` when in development mode or ``[tool.no-poetry.dev-dependencies]`` when not in development mode. This is because Poetry does not support either/or dependencies: either resolve dependency x in dev mode, or dependency y when not in dev mode. Instead, it takes a both/and approach: during its dependency resolution phase, it resolves ALL dependencies, then installs a subset (such all non-dev dependencies, or dev and non-dev dependencies). Quoting from the `manual <https://python-poetry.org/docs/master/managing-dependencies/>`_:
173257
#
174258
# All dependencies must be compatible with each other across groups since they will be resolved regardless of whether they are required for installation or not (see Installing group dependencies).
22.6 KB
Loading
31.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)