Commit fcc2c839 by Arun Babu Neelicattu Committed by Steph Samson

add command to add, remove and show project sources

parent b753aaf4
...@@ -581,3 +581,56 @@ The `plugin remove` command removes installed plugins. ...@@ -581,3 +581,56 @@ The `plugin remove` command removes installed plugins.
```bash ```bash
poetry plugin remove poetry-plugin poetry plugin remove poetry-plugin
``` ```
## source
The `source` namespace regroups sub commands to manage repository sources for a Poetry project.
### `source add`
The `source add` command adds source configuration to the project.
For example, to add the `pypi-test` source, you can run:
```bash
poetry source add pypi-test https://test.pypi.org/simple/
```
!!!note
You cannot use the name `pypi` as it is reserved for use by the default PyPI source.
#### Options
* `--default`: Set this source as the [default](/docs/repositories/#disabling-the-pypi-repository) (disable PyPI).
* `--secondary`: Set this source as a [secondary](/docs/repositories/#install-dependencies-from-a-private-repository) source.
!!!note
You cannot set a source as both `default` and `secondary`.
### `source show`
The `source show` command displays information on all configured sources for the project.
```bash
poetry source show
```
Optionally, you can show information of one or more sources by specifying their names.
```bash
poetry source show pypi-test
```
!!!note
This command will only show sources configured via the `pyproject.toml` and does not include PyPI.
### `source remove`
The `source remove` command removes a configured source from your `pyproject.toml`.
```bash
poetry source remove pypi-test
```
...@@ -714,7 +714,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt ...@@ -714,7 +714,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.6" python-versions = "^3.6"
content-hash = "8442060c68d80744b05aac3a07818a1e04e457c05b0e481d717cb44721009566" content-hash = "cccd84fe6459fdb43ff8bb775ee093c7ee351f112a054ffc6ca5ecf59deba1d5"
[metadata.files] [metadata.files]
appdirs = [ appdirs = [
......
import dataclasses
from typing import Dict
from typing import Union
@dataclasses.dataclass(order=True, eq=True)
class Source:
name: str
url: str
default: bool = dataclasses.field(default=False)
secondary: bool = dataclasses.field(default=False)
def to_dict(self) -> Dict[str, Union[str, bool]]:
return dataclasses.asdict(self)
...@@ -76,9 +76,12 @@ COMMANDS = [ ...@@ -76,9 +76,12 @@ COMMANDS = [
"plugin show", "plugin show",
# Self commands # Self commands
"self update", "self update",
# Source commands
"source add",
"source remove",
"source show",
] ]
if TYPE_CHECKING: if TYPE_CHECKING:
from cleo.io.inputs.definition import Definition from cleo.io.inputs.definition import Definition
......
from typing import Optional
from cleo.helpers import argument
from cleo.helpers import option
from cleo.io.null_io import NullIO
from tomlkit import nl
from tomlkit import table
from tomlkit.items import AoT
from tomlkit.items import Table
from poetry.config.source import Source
from poetry.console.commands.command import Command
from poetry.factory import Factory
from poetry.repositories import Pool
class SourceAddCommand(Command):
name = "source add"
description = "Add source configuration for project."
arguments = [
argument(
"name",
"Source repository name.",
),
argument("url", "Source repository url."),
]
options = [
option(
"default",
"d",
"Set this source as the default (disable PyPI). A "
"default source will also be the fallback source if "
"you add other sources.",
),
option("secondary", "s", "Set this source as secondary."),
]
@staticmethod
def source_to_table(source: Source) -> Table:
source_table: Table = table()
for key, value in source.to_dict().items():
source_table.add(key, value)
source_table.add(nl())
return source_table
def handle(self) -> Optional[int]:
name = self.argument("name")
url = self.argument("url")
is_default = self.option("default")
is_secondary = self.option("secondary")
if is_default and is_secondary:
self.line_error(
"Cannot configure a source as both <c1>default</c1> and <c1>secondary</c1>."
)
return 1
new_source = Source(
name=name, url=url, default=is_default, secondary=is_secondary
)
existing_sources = self.poetry.get_sources()
sources = AoT([])
for source in existing_sources:
if source == new_source:
self.line(
f"Source with name <c1>{name}</c1> already exits. Skipping addition."
)
return 0
elif source.default and is_default:
self.line_error(
f"<error>Source with name <c1>{source.name}</c1> is already set to default. "
f"Only one default source can be configured at a time.</error>"
)
return 1
if source.name == name:
self.line(f"Source with name <c1>{name}</c1> already exits. Updating.")
source = new_source
new_source = None
sources.append(self.source_to_table(source))
if new_source is not None:
self.line(f"Adding source with name <c1>{name}</c1>.")
sources.append(self.source_to_table(new_source))
# ensure new source is valid. eg: invalid name etc.
self.poetry._pool = Pool()
try:
Factory.configure_sources(
self.poetry, sources, self.poetry.config, NullIO()
)
self.poetry.pool.repository(name)
except ValueError as e:
self.line_error(
f"<error>Failed to validate addition of <c1>{name}</c1>: {e}</error>"
)
return 1
self.poetry.pyproject.poetry_config["source"] = sources
self.poetry.pyproject.save()
return 0
from typing import Optional
from cleo.helpers import argument
from tomlkit import nl
from tomlkit import table
from tomlkit.items import AoT
from tomlkit.items import Table
from poetry.config.source import Source
from poetry.console.commands.command import Command
class SourceRemoveCommand(Command):
name = "source remove"
description = "Remove source configured for the project."
arguments = [
argument(
"name",
"Source repository name.",
),
]
@staticmethod
def source_to_table(source: Source) -> Table:
source_table: Table = table()
for key, value in source.to_dict().items():
source_table.add(key, value)
source_table.add(nl())
return source_table
def handle(self) -> Optional[int]:
name = self.argument("name")
sources = AoT([])
removed = False
for source in self.poetry.get_sources():
if source.name == name:
self.line(f"Removing source with name <c1>{source.name}</c1>.")
removed = True
continue
sources.append(self.source_to_table(source))
if not removed:
self.line_error(
f"<error>Source with name <c1>{name}</c1> was not found.</error>"
)
return 1
self.poetry.pyproject.poetry_config["source"] = sources
self.poetry.pyproject.save()
return 0
from typing import Optional
from cleo.helpers import argument
from poetry.console.commands.command import Command
class SourceShowCommand(Command):
name = "source show"
description = "Show information about sources configured for the project."
arguments = [
argument(
"source",
"Source(s) to show information for. Defaults to showing all sources.",
optional=True,
multiple=True,
),
]
def handle(self) -> Optional[int]:
sources = self.poetry.get_sources()
names = self.argument("source")
if not sources:
self.line("No sources configured for this project.")
return 0
if names and not any(map(lambda s: s.name in names, sources)):
self.line_error(f"No source found with name(s): {', '.join(names)}")
return 1
bool_string = {
True: "yes",
False: "no",
}
for source in sources:
if names and source.name not in names:
continue
table = self.table(style="compact")
rows = [
["<info>name</>", " : <c1>{}</>".format(source.name)],
["<info>url</>", " : {}".format(source.url)],
[
"<info>default</>",
" : {}".format(bool_string.get(source.default, False)),
],
[
"<info>secondary</>",
" : {}".format(bool_string.get(source.secondary, False)),
],
]
table.add_rows(rows)
table.render()
self.line("")
return 0
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import List
from poetry.__version__ import __version__
from poetry.config.source import Source
from poetry.core.poetry import Poetry as BasePoetry from poetry.core.poetry import Poetry as BasePoetry
from .__version__ import __version__
if TYPE_CHECKING: if TYPE_CHECKING:
from poetry.core.packages.project_package import ProjectPackage from poetry.core.packages.project_package import ProjectPackage
...@@ -67,3 +68,9 @@ class Poetry(BasePoetry): ...@@ -67,3 +68,9 @@ class Poetry(BasePoetry):
self._plugin_manager = plugin_manager self._plugin_manager = plugin_manager
return self return self
def get_sources(self) -> List[Source]:
return [
Source(**source)
for source in self.pyproject.poetry_config.get("source", [])
]
...@@ -44,6 +44,7 @@ virtualenv = "^20.4.3" ...@@ -44,6 +44,7 @@ virtualenv = "^20.4.3"
keyring = "^21.2.0" keyring = "^21.2.0"
entrypoints = "^0.3" entrypoints = "^0.3"
importlib-metadata = {version = "^1.6.0", python = "<3.8"} importlib-metadata = {version = "^1.6.0", python = "<3.8"}
dataclasses = {version = "^0.8", python = "~3.6"}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^5.4.3" pytest = "^5.4.3"
......
import pytest
from poetry.config.source import Source
@pytest.fixture
def source_one():
return Source(name="one", url="https://one.com")
@pytest.fixture
def source_two():
return Source(name="two", url="https://two.com")
@pytest.fixture
def source_default():
return Source(name="default", url="https://default.com", default=True)
@pytest.fixture
def source_secondary():
return Source(name="secondary", url="https://secondary.com", secondary=True)
_existing_source = Source(name="existing", url="https://existing.com")
@pytest.fixture
def source_existing():
return _existing_source
PYPROJECT_WITH_SOURCES = f"""
[tool.poetry]
name = "source-command-test"
version = "0.1.0"
description = ""
authors = ["Poetry Tester <tester@poetry.org>"]
[tool.poetry.dependencies]
python = "^3.9"
[tool.poetry.dev-dependencies]
[[tool.poetry.source]]
name = "{_existing_source.name}"
url = "{_existing_source.url}"
"""
@pytest.fixture
def poetry_with_source(project_factory):
return project_factory(pyproject_content=PYPROJECT_WITH_SOURCES)
@pytest.fixture
def add_multiple_sources(
command_tester_factory, poetry_with_source, source_one, source_two
):
add = command_tester_factory("source add", poetry=poetry_with_source)
for source in [source_one, source_two]:
add.execute(f"{source.name} {source.url}")
import dataclasses
import pytest
@pytest.fixture
def tester(command_tester_factory, poetry_with_source):
return command_tester_factory("source add", poetry=poetry_with_source)
def assert_source_added(tester, poetry, source_existing, source_added):
assert (
tester.io.fetch_output().strip()
== f"Adding source with name {source_added.name}."
)
poetry.pyproject.reload()
sources = poetry.get_sources()
assert sources == [source_existing, source_added]
assert tester.status_code == 0
def test_source_add_simple(tester, source_existing, source_one, poetry_with_source):
tester.execute(f"{source_one.name} {source_one.url}")
assert_source_added(tester, poetry_with_source, source_existing, source_one)
def test_source_add_default(
tester, source_existing, source_default, poetry_with_source
):
tester.execute(f"--default {source_default.name} {source_default.url}")
assert_source_added(tester, poetry_with_source, source_existing, source_default)
def test_source_add_secondary(
tester, source_existing, source_secondary, poetry_with_source
):
tester.execute(f"--secondary {source_secondary.name} {source_secondary.url}")
assert_source_added(tester, poetry_with_source, source_existing, source_secondary)
def test_source_add_error_default_and_secondary(tester):
tester.execute("--default --secondary error https://error.com")
assert (
tester.io.fetch_error().strip()
== "Cannot configure a source as both default and secondary."
)
assert tester.status_code == 1
def test_source_add_error_pypi(tester):
tester.execute("pypi https://test.pypi.org/simple/")
assert (
tester.io.fetch_error().strip()
== "Failed to validate addition of pypi: The name [pypi] is reserved for repositories"
)
assert tester.status_code == 1
def test_source_add_existing(tester, source_existing, poetry_with_source):
tester.execute(f"--default {source_existing.name} {source_existing.url}")
assert (
tester.io.fetch_output().strip()
== f"Source with name {source_existing.name} already exits. Updating."
)
poetry_with_source.pyproject.reload()
sources = poetry_with_source.get_sources()
assert len(sources) == 1
assert sources[0] != source_existing
assert sources[0] == dataclasses.replace(source_existing, default=True)
import pytest
@pytest.fixture
def tester(command_tester_factory, poetry_with_source, add_multiple_sources):
return command_tester_factory("source remove", poetry=poetry_with_source)
def test_source_remove_simple(
tester, poetry_with_source, source_existing, source_one, source_two
):
tester.execute(f"{source_existing.name}")
assert (
tester.io.fetch_output().strip()
== f"Removing source with name {source_existing.name}."
)
poetry_with_source.pyproject.reload()
sources = poetry_with_source.get_sources()
assert sources == [source_one, source_two]
assert tester.status_code == 0
def test_source_remove_error(tester):
tester.execute("error")
assert tester.io.fetch_error().strip() == "Source with name error was not found."
assert tester.status_code == 1
import pytest
@pytest.fixture
def tester(command_tester_factory, poetry_with_source, add_multiple_sources):
return command_tester_factory("source show", poetry=poetry_with_source)
def test_source_show_simple(tester):
tester.execute("")
expected = """\
name : existing
url : https://existing.com
default : no
secondary : no
name : one
url : https://one.com
default : no
secondary : no
name : two
url : https://two.com
default : no
secondary : no
""".splitlines()
assert (
list(map(lambda l: l.strip(), tester.io.fetch_output().strip().splitlines()))
== expected
)
assert tester.status_code == 0
def test_source_show_one(tester, source_one):
tester.execute(f"{source_one.name}")
expected = """\
name : one
url : https://one.com
default : no
secondary : no
""".splitlines()
assert (
list(map(lambda l: l.strip(), tester.io.fetch_output().strip().splitlines()))
== expected
)
assert tester.status_code == 0
def test_source_show_two(tester, source_one, source_two):
tester.execute(f"{source_one.name} {source_two.name}")
expected = """\
name : one
url : https://one.com
default : no
secondary : no
name : two
url : https://two.com
default : no
secondary : no
""".splitlines()
assert (
list(map(lambda l: l.strip(), tester.io.fetch_output().strip().splitlines()))
== expected
)
assert tester.status_code == 0
def test_source_show_error(tester):
tester.execute("error")
assert tester.io.fetch_error().strip() == "No source found with name(s): error"
assert tester.status_code == 1
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment