Skip to content

Commit 1d59d6d

Browse files
committed
Support for parsin setup.cfg and python-version, detection of parser
1 parent 6f5c02e commit 1d59d6d

File tree

3 files changed

+101
-4
lines changed

3 files changed

+101
-4
lines changed

rsconnect/pyproject.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import pathlib
99
import typing
10+
import configparser
1011

1112
try:
1213
import tomllib
@@ -24,14 +25,31 @@ def lookup_metadata_file(directory: typing.Union[str, pathlib.Path]) -> typing.L
2425
directory = pathlib.Path(directory)
2526

2627
def _generate():
27-
for filename in ("pyproject.toml", "setup.cfg", ".python-version"):
28+
for filename in (".python-version", "pyproject.toml", "setup.cfg"):
2829
path = directory / filename
2930
if path.is_file():
3031
yield (filename, path)
3132

3233
return list(_generate())
3334

3435

36+
def get_python_requires_parser(
37+
metadata_file: pathlib.Path,
38+
) -> typing.Optional[typing.Callable[[pathlib.Path], typing.Optional[str]]]:
39+
"""Given the metadata file, return the appropriate parser function.
40+
41+
The returned function takes a pathlib.Path and returns the parsed value.
42+
"""
43+
if metadata_file.name == "pyproject.toml":
44+
return parse_pyproject_python_requires
45+
elif metadata_file.name == "setup.cfg":
46+
return parse_setupcfg_python_requires
47+
elif metadata_file.name == ".python-version":
48+
return parse_pyversion_python_requires
49+
else:
50+
return None
51+
52+
3553
def parse_pyproject_python_requires(pyproject_file: pathlib.Path) -> typing.Optional[str]:
3654
"""Parse the project.requires-python field from a pyproject.toml file.
3755
@@ -43,3 +61,27 @@ def parse_pyproject_python_requires(pyproject_file: pathlib.Path) -> typing.Opti
4361
pyproject = tomllib.loads(content)
4462

4563
return pyproject.get("project", {}).get("requires-python", None)
64+
65+
66+
def parse_setupcfg_python_requires(setupcfg_file: pathlib.Path) -> typing.Optional[str]:
67+
"""Parse the python_requires field from a setup.cfg file.
68+
69+
Assumes that the setup.cfg file exists, is accessible and well formatted.
70+
71+
Returns None if the field is not found.
72+
"""
73+
config = configparser.ConfigParser()
74+
config.read(setupcfg_file)
75+
76+
return config.get("options", "python_requires", fallback=None)
77+
78+
79+
def parse_pyversion_python_requires(pyversion_file: pathlib.Path) -> typing.Optional[str]:
80+
"""Parse the python version from a .python-version file.
81+
82+
Assumes that the .python-version file exists, is accessible and well formatted.
83+
84+
Returns None if the field is not found.
85+
"""
86+
content = pyversion_file.read_text()
87+
return content.strip()

tests/test_pyproject.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import os
22
import pathlib
3+
import tempfile
34

4-
from rsconnect.pyproject import lookup_metadata_file, parse_pyproject_python_requires
5+
from rsconnect.pyproject import (
6+
lookup_metadata_file,
7+
parse_pyproject_python_requires,
8+
parse_setupcfg_python_requires,
9+
parse_pyversion_python_requires,
10+
get_python_requires_parser,
11+
)
512

613
import pytest
714

@@ -23,11 +30,12 @@
2330
(
2431
os.path.join(PROJECTS_DIRECTORY, "using_pyversion"),
2532
(
26-
"pyproject.toml",
2733
".python-version",
34+
"pyproject.toml",
35+
"setup.cfg",
2836
),
2937
),
30-
(os.path.join(PROJECTS_DIRECTORY, "allofthem"), ("pyproject.toml", "setup.cfg", ".python-version")),
38+
(os.path.join(PROJECTS_DIRECTORY, "allofthem"), (".python-version", "pyproject.toml", "setup.cfg")),
3139
],
3240
ids=["pyproject.toml", "setup.cfg", ".python-version", "allofthem"],
3341
)
@@ -37,6 +45,22 @@ def test_python_project_metadata_detect(project_dir, expected):
3745
assert lookup_metadata_file(project_dir) == expectation
3846

3947

48+
@pytest.mark.parametrize(
49+
"filename, expected_parser",
50+
[
51+
("pyproject.toml", parse_pyproject_python_requires),
52+
("setup.cfg", parse_setupcfg_python_requires),
53+
(".python-version", parse_pyversion_python_requires),
54+
("invalid.txt", None),
55+
],
56+
ids=["pyproject.toml", "setup.cfg", ".python-version", "invalid"],
57+
)
58+
def test_get_python_requires_parser(filename, expected_parser):
59+
metadata_file = pathlib.Path(PROJECTS_DIRECTORY) / filename
60+
parser = get_python_requires_parser(metadata_file)
61+
assert parser == expected_parser
62+
63+
4064
@pytest.mark.parametrize(
4165
"project_dir",
4266
[
@@ -65,3 +89,30 @@ def test_pyprojecttoml_python_requires(project_dir, expected):
6589
"""
6690
pyproject_file = pathlib.Path(project_dir) / "pyproject.toml"
6791
assert parse_pyproject_python_requires(pyproject_file) == expected
92+
93+
94+
@pytest.mark.parametrize(
95+
"project_dir, expected",
96+
[
97+
(os.path.join(PROJECTS_DIRECTORY, "using_setupcfg"), ">=3.8"),
98+
(os.path.join(PROJECTS_DIRECTORY, "using_pyversion"), None),
99+
],
100+
ids=["option-exists", "option-missing"],
101+
)
102+
def test_setupcfg_python_requires(tmp_path, project_dir, expected):
103+
setupcfg_file = pathlib.Path(project_dir) / "setup.cfg"
104+
assert parse_setupcfg_python_requires(setupcfg_file) == expected
105+
106+
107+
@pytest.mark.parametrize(
108+
"project_dir, expected",
109+
[
110+
(os.path.join(PROJECTS_DIRECTORY, "using_pyversion"), ">=3.8, <3.12"),
111+
# There is no case (option-missing) where the .python-version file is empty,
112+
# so we don't test that.
113+
],
114+
ids=["option-exists"],
115+
)
116+
def test_pyversion_python_requires(tmp_path, project_dir, expected):
117+
versionfile = pathlib.Path(project_dir) / ".python-version"
118+
assert parse_pyversion_python_requires(versionfile) == expected
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[metadata]
2+
name = python-project
3+
version = 0.1.0
4+
description = Add your description here

0 commit comments

Comments
 (0)