Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 36: Implement entrypoint support #46

Merged
merged 22 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4df1132
__init__: Implement entrypoint support
sjlongland Aug 5, 2023
b436a98
__init__: Use string literals to keep `mypy` quiet.
sjlongland Aug 5, 2023
a1517c2
setup_command tests: Fix the "everything" test
sjlongland Aug 5, 2023
b73966d
__init__: Make `_parse_entrypoint` a static method
sjlongland Aug 6, 2023
46d321b
WritePyproject unit tests: Test entry point parsing/emitting.
sjlongland Aug 6, 2023
f29294e
WritePyproject unit tests: Actually commit the new test suite.
sjlongland Aug 6, 2023
4266249
WritePyproject unit tests: drop redundant import
sjlongland Aug 6, 2023
d38aeb0
__init__: split long line
sjlongland Aug 6, 2023
772d5a5
__init__: Add example code block to _parse_entrypoint docstring.
sjlongland Aug 6, 2023
219c943
__init__: Make _parse_entrypoint return a `Tuple[str, str]`
sjlongland Aug 6, 2023
2c5fb2a
__init__: Re-factor `_generate_entrypoints` to use `dict(map(…))`
sjlongland Aug 6, 2023
1756058
__init__: WritePyproject: Adjust _parse_entrypoint docs.
sjlongland Aug 11, 2023
b628441
__init__: WritePyproject: `entrypoints` → `entry_points`
sjlongland Aug 11, 2023
8bdd0cd
__init__: WritePyproject: `parsedentrypoints` → `parsed_entry_points`
sjlongland Aug 11, 2023
cccf8fd
__init__: Move entry-point functions out of the class
sjlongland Aug 11, 2023
0c81a11
Entry point tests: Re-factor test input/expected output to variables.
sjlongland Aug 11, 2023
587ed66
Entry point tests: Re-factor unhappy path to use `pytest.raises`
sjlongland Aug 11, 2023
1ffe28f
Entry point tests: `project` → `result`
sjlongland Aug 11, 2023
517cc6e
Entry point tests: Convert _parse_entrypoint tests to doctests.
sjlongland Aug 11, 2023
f323e8a
entry point tests: Convert _generate_entrypoint tests to doctests.
sjlongland Aug 11, 2023
bb6dc66
Clean up "entry points" consistency.
sjlongland Aug 11, 2023
13856b6
Entry point tests: Remove redundant import
sjlongland Aug 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/setuptools_pyproject_migration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,61 @@
LicenseText: Type = TypedDict("LicenseText", {"text": str})
ReadmeInfo: Type = TypedDict("ReadmeInfo", {"file": str, "content-type": str})


def _parse_entry_point(entry_point: str) -> Tuple[str, str]:
"""
Extract the entry point and name from the string.

>>> _parse_entry_point("hello-world = timmins:hello_world")
('hello-world', 'timmins:hello_world')
>>> _parse_entry_point("hello-world=timmins:hello_world")
('hello-world', 'timmins:hello_world')
>>> _parse_entry_point(" hello-world = timmins:hello_world ")
('hello-world', 'timmins:hello_world')
>>> _parse_entry_point("hello-world")
Traceback (most recent call last):
...
ValueError: Entry point 'hello-world' is not of the form 'name = module:function'

:param: entry_point The entry point string, of the form
"entry_point = module:function" (whitespace optional)
:returns: A two-element `tuple`, first element is the entry point name, second element is the target
(module and function name) as a string.
:raises ValueError: An equals (`=`) character was not present in the entry point string.
"""
if "=" not in entry_point:
raise ValueError("Entry point %r is not of the form 'name = module:function'" % entry_point)

(name, target) = entry_point.split("=", 1)
return (name.strip(), target.strip())


def _generate_entry_points(entry_points: Optional[Dict[str, List[str]]]) -> Dict[str, Dict[str, str]]:
"""
Dump the entry points given, if any.

>>> _generate_entry_points(None)
{}
>>> _generate_entry_points({"type1": ["ep1=mod:fn1", "ep2=mod:fn2"],
... "type2": ["ep3=mod:fn3", "ep4=mod:fn4"]})
{'type1': {'ep1': 'mod:fn1', 'ep2': 'mod:fn2'}, 'type2': {'ep3': 'mod:fn3', 'ep4': 'mod:fn4'}}

:param: entry_points The `entry_points` property from the
:py:class:setuptools.dist.Distribution being examined.
:returns: The entry points, split up as per
:py:func:_parse_entry_point and grouped by entry point type.
"""
if not entry_points:
return {}

parsed_entry_points: Dict[str, Dict[str, str]] = {}

for eptype, raweps in entry_points.items():
parsed_entry_points[eptype] = dict(map(_parse_entry_point, raweps))

return parsed_entry_points


Project: Type = TypedDict(
"Project",
{
Expand Down Expand Up @@ -147,6 +202,19 @@ def _generate(self) -> Pyproject:
if dependencies:
pyproject["project"]["dependencies"] = dependencies

entry_points = _generate_entry_points(dist.entry_points)

# GUI scripts and console scripts go separate in dedicated locations.
if "console_scripts" in entry_points:
pyproject["project"]["scripts"] = entry_points.pop("console_scripts")

if "gui_scripts" in entry_points:
pyproject["project"]["gui-scripts"] = entry_points.pop("gui_scripts")

# Anything left over gets put in entry-points
if entry_points:
pyproject["project"]["entry-points"] = entry_points

return pyproject

def run(self):
Expand Down
163 changes: 163 additions & 0 deletions tests/test_entry_points.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
Testing the logic that extracts the entrypoint data:
- CLI entrypoints
- GUI entrypoints
- miscellaneous entrypoints
"""

from setuptools_pyproject_migration import WritePyproject
from setuptools.dist import Distribution


def test_generate_noentrypoints():
"""
Test distribution with no entry points generates no entry points
"""
cmd = WritePyproject(
Distribution(
dict(
name="TestProject",
version="1.2.3",
)
)
)
result = cmd._generate()

assert "scripts" not in result["project"]
assert "gui-scripts" not in result["project"]
assert "entry-points" not in result["project"]


def test_generate_clionly():
"""
Test distribution with only CLI scripts generates only "scripts"
"""
cmd = WritePyproject(
Distribution(
dict(
name="TestProject",
version="1.2.3",
entry_points=dict(
console_scripts=[
"spanish-inquisition=montypython.unexpected:spanishinquisition",
"brian=montypython.naughtyboy:brian",
]
),
)
)
)
result = cmd._generate()

assert "gui-scripts" not in result["project"]
assert "entry-points" not in result["project"]

assert result["project"]["scripts"] == {
"spanish-inquisition": "montypython.unexpected:spanishinquisition",
"brian": "montypython.naughtyboy:brian",
}


def test_generate_guionly():
"""
Test distribution with only GUI scripts generates only "gui-scripts"
"""
cmd = WritePyproject(
Distribution(
dict(
name="TestProject",
version="1.2.3",
entry_points=dict(
gui_scripts=[
"dead-parrot=montypython.sketch:petshop",
"shrubbery=montypython.holygrail:knightswhosayni",
]
),
)
)
)
result = cmd._generate()

assert "scripts" not in result["project"]
assert "entry-points" not in result["project"]

assert result["project"]["gui-scripts"] == {
"dead-parrot": "montypython.sketch:petshop",
"shrubbery": "montypython.holygrail:knightswhosayni",
}


def test_generate_misconly():
"""
Test distribution with only misc entry points generates only "entry-points"
"""
cmd = WritePyproject(
Distribution(
dict(
name="TestProject",
version="1.2.3",
entry_points={
"project.plugins": [
"babysnatchers=montypython.somethingcompletelydifferent:babysnatchers",
"eels=montypython.somethingcompletelydifferent:eels",
]
},
)
)
)
result = cmd._generate()

assert "scripts" not in result["project"]
assert "gui-scripts" not in result["project"]

assert result["project"]["entry-points"] == {
"project.plugins": {
"babysnatchers": "montypython.somethingcompletelydifferent:babysnatchers",
"eels": "montypython.somethingcompletelydifferent:eels",
},
}


def test_generate_all_entrypoints():
"""
Test distribution with all entry point types, generates all sections
"""
cmd = WritePyproject(
Distribution(
dict(
name="TestProject",
version="1.2.3",
entry_points={
"console_scripts": [
"spanish-inquisition=montypython.unexpected:spanishinquisition",
"brian=montypython.naughtyboy:brian",
],
"gui_scripts": [
"dead-parrot=montypython.sketch:petshop",
"shrubbery=montypython.holygrail:knightswhosayni",
],
"project.plugins": [
"babysnatchers=montypython.somethingcompletelydifferent:babysnatchers",
"eels=montypython.somethingcompletelydifferent:eels",
],
},
)
)
)
result = cmd._generate()

assert result["project"]["scripts"] == {
"spanish-inquisition": "montypython.unexpected:spanishinquisition",
"brian": "montypython.naughtyboy:brian",
}

assert result["project"]["gui-scripts"] == {
"dead-parrot": "montypython.sketch:petshop",
"shrubbery": "montypython.holygrail:knightswhosayni",
}

assert result["project"]["entry-points"] == {
"project.plugins": {
"babysnatchers": "montypython.somethingcompletelydifferent:babysnatchers",
"eels": "montypython.somethingcompletelydifferent:eels",
},
}
20 changes: 20 additions & 0 deletions tests/test_setup_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ def test_everything(project) -> None:
sphinx-lint

[options.entry_points]
console_scripts =
cliep1 = test_project.cliep1
cliep2 = test_project.cliep2

gui_scripts =
guiep1 = test_project.guiep1
guiep2 = test_project.guiep2

test_project.dummy =
ep1 = test_project.ep1
ep2 = test_project.ep2
Expand Down Expand Up @@ -166,6 +174,18 @@ def test_everything(project) -> None:
[[project.maintainers]]
name = "Stuart Longland"
email = "me@vk4msl.com"

[project.scripts]
cliep1 = "test_project.cliep1"
cliep2 = "test_project.cliep2"

[project.gui-scripts]
guiep1 = "test_project.guiep1"
guiep2 = "test_project.guiep2"

[project.entry-points."test_project.dummy"]
ep1 = "test_project.ep1"
ep2 = "test_project.ep2"
"""
project.setup_cfg(setup_cfg)
project.write("README.md", readme_md)
Expand Down