Skip to content

Commit 11fe01d

Browse files
feat: Add a poetry-specific parser, and a mechanism for selecting it. (#33)
* feat: Add a poetry-specific parser, and a mechanism for selecting it. * Add documentation. * Add test for poetry configuration with `maintainers` rather than `authors` * Fix typo * Document `style` parameter --------- Co-authored-by: Dominic Davis-Foster <[email protected]>
1 parent b1add35 commit 11fe01d

File tree

4 files changed

+142
-5
lines changed

4 files changed

+142
-5
lines changed

doc-source/usage.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ By passing :func:`globalns=globals() <globals>` to the class constructor, the ke
3535
This will prevent warnings from linters etc., but is not necessary for Sphinx to see the configuration.
3636

3737

38+
.. note::
39+
40+
At time of writing the "Poetry" tool does not support PEP 621. To enable a mode compatible with
41+
the ``[tool.poetry]`` heading supply the argument ``style="poetry"``. For example:
42+
43+
.. code-block:: python
44+
45+
config = SphinxConfig("../pyproject.toml", style="poetry")
46+
47+
3848
Configuration
3949
----------------
4050

sphinx_pyproject/__init__.py

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#
2828

2929
# stdlib
30+
import re
3031
from typing import Any, Dict, Iterator, List, Mapping, MutableMapping, Optional
3132

3233
# 3rd party
@@ -43,7 +44,7 @@
4344
__version__: str = "0.1.0"
4445
__email__: str = "[email protected]"
4546

46-
__all__ = ["SphinxConfig", "ProjectParser"]
47+
__all__ = ["SphinxConfig", "ProjectParser", "PoetryProjectParser"]
4748

4849

4950
class SphinxConfig(Mapping[str, Any]):
@@ -55,6 +56,9 @@ class SphinxConfig(Mapping[str, Any]):
5556
The variables parsed from the ``[tool.sphinx-pyproject]`` table will be added to this namespace.
5657
By default, or if explicitly :py:obj:`None`, this does not happen.
5758
:no-default globalns:
59+
:param style: Either ``pep621`` (default), or ``poetry`` to read configuration from the ``[tool.poetry]`` table.
60+
:no-default style:
61+
5862
5963
.. autosummary-widths:: 1/4
6064
"""
@@ -122,15 +126,19 @@ def __init__(
122126
pyproject_file: PathLike = "../pyproject.toml",
123127
*,
124128
globalns: Optional[MutableMapping] = None,
129+
style: str = "pep621",
125130
):
126131

127132
pyproject_file = PathPlus(pyproject_file).abspath()
128133
config = dom_toml.load(pyproject_file, decoder=TomlPureDecoder)
129134

130-
if "project" not in config:
131-
raise BadConfigError(f"No 'project' table found in {pyproject_file.as_posix()}")
135+
parser_cls = project_parser_styles.get(style)
136+
if parser_cls is None:
137+
styles = ", ".join(project_parser_styles)
138+
raise ValueError(f"'style' argument must be one of: {styles}")
132139

133-
pep621_config = ProjectParser().parse(config["project"])
140+
namespace = parser_cls.get_namespace(pyproject_file, config)
141+
pep621_config = parser_cls().parse(namespace)
134142

135143
for key in ("name", "version", "description"):
136144
if key not in pep621_config:
@@ -191,6 +199,22 @@ class ProjectParser(AbstractConfigParser):
191199
.. autosummary-widths:: 7/16
192200
"""
193201

202+
@staticmethod
203+
def get_namespace(filename: PathPlus, config: Dict[str, TOML_TYPES]) -> Dict[str, TOML_TYPES]:
204+
"""
205+
Returns the ``[project]`` table in a ``project.toml`` file.
206+
207+
:param filename: The filename the TOML data was read from. Used in error messages.
208+
:param config: The data from the TOML file.
209+
210+
.. versionadded:: 0.2.0
211+
"""
212+
213+
if "project" not in config:
214+
raise BadConfigError(f"No 'project' table found in {filename.as_posix()}")
215+
216+
return config["project"]
217+
194218
def parse_name(self, config: Dict[str, TOML_TYPES]) -> str:
195219
"""
196220
Parse the :pep621:`name` key.
@@ -273,10 +297,57 @@ def parse(
273297
:param config:
274298
:param set_defaults: Has no effect in this class.
275299
"""
276-
277300
if "authors" in config:
278301
config["author"] = config.pop("authors")
279302
elif "maintainers" in config:
280303
config["author"] = config.pop("maintainers")
281304

282305
return super().parse(config)
306+
307+
308+
class PoetryProjectParser(ProjectParser):
309+
"""
310+
Parser for poetry metadata from ``pyproject.toml``.
311+
312+
.. versionadded:: 0.2.0
313+
"""
314+
315+
@staticmethod
316+
def get_namespace(filename: PathPlus, config: Dict[str, TOML_TYPES]) -> Dict[str, TOML_TYPES]:
317+
"""
318+
Returns the ``[tool.poetry]`` table in a ``project.toml`` file.
319+
320+
:param filename: The filename the TOML data was read from. Used in error messages.
321+
:param config: The data from the TOML file.
322+
"""
323+
324+
result = config.get("tool", {}).get("poetry")
325+
if result is None:
326+
raise BadConfigError(f"No 'tool.poetry' table found in {filename.as_posix()}")
327+
328+
return result
329+
330+
@staticmethod
331+
def parse_author(config: Dict[str, TOML_TYPES]) -> str:
332+
"""
333+
Parse poetry's authors key.
334+
335+
:param config: The unparsed TOML config for the ``[tool.poetry]`` table.
336+
"""
337+
338+
pep621_style_authors: List[Dict[str, str]] = []
339+
340+
for author in config["author"]:
341+
match = re.match(r"(?P<name>.*)<(?P<email>.*)>", author)
342+
if match:
343+
name = match.group("name").strip()
344+
email = match.group("email").strip()
345+
pep621_style_authors.append({"name": name, "email": email})
346+
347+
return ProjectParser.parse_author({"author": pep621_style_authors})
348+
349+
350+
project_parser_styles = {
351+
"pep621": ProjectParser,
352+
"poetry": PoetryProjectParser,
353+
}

tests/test_sphinx_pyproject.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# stdlib
2+
import textwrap
23
from typing import Any, Dict
34

45
# 3rd party
@@ -153,3 +154,57 @@ def test_missing_keys(tmp_pathplus: PathPlus, config: str):
153154

154155
with pytest.raises(BadConfigError, match=err):
155156
SphinxConfig(tmp_pathplus / "pyproject.toml")
157+
158+
159+
POETRY_AUTHORS = """
160+
[tool.poetry]
161+
name = 'foo'
162+
version = '1.2.3'
163+
description = 'desc'
164+
authors = ["Person <[email protected]>"]
165+
"""
166+
167+
POETRY_MAINTAINERS = """
168+
[tool.poetry]
169+
name = 'foo'
170+
version = '1.2.3'
171+
description = 'desc'
172+
maintainers = ["Person <[email protected]>"]
173+
"""
174+
175+
176+
@pytest.mark.parametrize(
177+
"toml", [
178+
pytest.param(POETRY_AUTHORS, id="authors"),
179+
pytest.param(POETRY_MAINTAINERS, id="maintainers"),
180+
]
181+
)
182+
def test_poetry(tmp_pathplus: PathPlus, toml: str):
183+
(tmp_pathplus / "pyproject.toml").write_text(toml)
184+
185+
config = SphinxConfig(tmp_pathplus / "pyproject.toml", style="poetry")
186+
assert config.name == "foo"
187+
assert config.version == "1.2.3"
188+
assert config.author == "Person"
189+
assert config.description == "desc"
190+
191+
192+
def test_poetry_missing_heading(tmp_pathplus: PathPlus):
193+
toml = textwrap.dedent("""
194+
[other.table]
195+
name = 'foo'
196+
""")
197+
198+
(tmp_pathplus / "pyproject.toml").write_text(toml)
199+
200+
err = "No 'tool.poetry' table found in"
201+
with pytest.raises(BadConfigError, match=err):
202+
SphinxConfig(tmp_pathplus / "pyproject.toml", style="poetry")
203+
204+
205+
def test_invalid_style(tmp_pathplus: PathPlus):
206+
(tmp_pathplus / "pyproject.toml").write_text('')
207+
208+
err = "'style' argument must be one of: pep621, poetry"
209+
with pytest.raises(ValueError, match=err):
210+
SphinxConfig(tmp_pathplus / "pyproject.toml", style="other")

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ plugins = coverage_pyver_pragma
210210
211211
[coverage:report]
212212
fail_under = 100
213+
show_missing = true
213214
exclude_lines =
214215
raise AssertionError
215216
raise NotImplementedError

0 commit comments

Comments
 (0)