Skip to content

Commit b06a9f1

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

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

@@ -17,11 +24,12 @@
1724
(
1825
os.path.join(PROJECTS_DIRECTORY, "using_pyversion"),
1926
(
20-
"pyproject.toml",
2127
".python-version",
28+
"pyproject.toml",
29+
"setup.cfg",
2230
),
2331
),
24-
(os.path.join(PROJECTS_DIRECTORY, "allofthem"), ("pyproject.toml", "setup.cfg", ".python-version")),
32+
(os.path.join(PROJECTS_DIRECTORY, "allofthem"), (".python-version", "pyproject.toml", "setup.cfg")),
2533
],
2634
ids=["pyproject.toml", "setup.cfg", ".python-version", "allofthem"],
2735
)
@@ -30,6 +38,22 @@ def test_python_project_metadata_detect(project_dir, expected):
3038
assert lookup_metadata_file(project_dir) == expectation
3139

3240

41+
@pytest.mark.parametrize(
42+
"filename, expected_parser",
43+
[
44+
("pyproject.toml", parse_pyproject_python_requires),
45+
("setup.cfg", parse_setupcfg_python_requires),
46+
(".python-version", parse_pyversion_python_requires),
47+
("invalid.txt", None),
48+
],
49+
ids=["pyproject.toml", "setup.cfg", ".python-version", "invalid"],
50+
)
51+
def test_get_python_requires_parser(filename, expected_parser):
52+
metadata_file = pathlib.Path(PROJECTS_DIRECTORY) / filename
53+
parser = get_python_requires_parser(metadata_file)
54+
assert parser == expected_parser
55+
56+
3357
@pytest.mark.parametrize(
3458
"project_dir",
3559
[
@@ -53,3 +77,30 @@ def test_python_project_metadata_missing(project_dir):
5377
def test_pyprojecttoml_python_requires(project_dir, expected):
5478
pyproject_file = pathlib.Path(project_dir) / "pyproject.toml"
5579
assert parse_pyproject_python_requires(pyproject_file) == expected
80+
81+
82+
@pytest.mark.parametrize(
83+
"project_dir, expected",
84+
[
85+
(os.path.join(PROJECTS_DIRECTORY, "using_setupcfg"), ">=3.8"),
86+
(os.path.join(PROJECTS_DIRECTORY, "using_pyversion"), None),
87+
],
88+
ids=["option-exists", "option-missing"],
89+
)
90+
def test_setupcfg_python_requires(tmp_path, project_dir, expected):
91+
setupcfg_file = pathlib.Path(project_dir) / "setup.cfg"
92+
assert parse_setupcfg_python_requires(setupcfg_file) == expected
93+
94+
95+
@pytest.mark.parametrize(
96+
"project_dir, expected",
97+
[
98+
(os.path.join(PROJECTS_DIRECTORY, "using_pyversion"), ">=3.8, <3.12"),
99+
# There is no case (option-missing) where the .python-version file is empty,
100+
# so we don't test that.
101+
],
102+
ids=["option-exists"],
103+
)
104+
def test_pyversion_python_requires(tmp_path, project_dir, expected):
105+
versionfile = pathlib.Path(project_dir) / ".python-version"
106+
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)