Commit 978d435b by Randy Döring Committed by GitHub

sources: allow to configure the priority of PyPI (#7801)

Add a warning that PyPI will be disabled automatically in a future version of Poetry if there is at least one custom source configured with another priority than `explicit` and that it should be configured explicitly with a certain priority for forward compatibility.
parent 605823e4
......@@ -779,9 +779,12 @@ For example, to add the `pypi-test` source, you can run:
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.
{{% /note %}}
You cannot use the name `pypi` for a custom repository as it is reserved for use by
the default PyPI source. However, you can set the priority of PyPI:
```bash
poetry source add --priority=explicit pypi
```
#### Options
......@@ -808,7 +811,8 @@ poetry source show pypi-test
```
{{% note %}}
This command will only show sources configured via the `pyproject.toml` and does not include PyPI.
This command will only show sources configured via the `pyproject.toml`
and does not include the implicit default PyPI.
{{% /note %}}
### source remove
......
......@@ -128,7 +128,7 @@ If `priority` is undefined, the source is considered a primary source that takes
Package sources are considered in the following order:
1. [default source](#default-package-source),
2. primary sources,
3. PyPI (unless disabled by another default source),
3. implicit PyPI (unless disabled by another [default source](#default-package-source) or configured explicitly),
4. [secondary sources](#secondary-package-sources),
[Explicit sources](#explicit-package-sources) are considered only for packages that explicitly [indicate their source](#package-source-constraint).
......@@ -137,19 +137,17 @@ Within each priority class, package sources are considered in order of appearanc
{{% note %}}
If you prefer to disable [PyPI](https://pypi.org) completely, you may choose to set one of your package sources to be the [default](#default-package-source).
If you want to change the priority of [PyPI](https://pypi.org), you can set it explicitly, e.g.
If you prefer to specify a package source for a specific dependency, see [Secondary Package Sources](#secondary-package-sources).
{{% /note %}}
{{% warning %}}
```bash
poetry source add --priority=primary PyPI
```
If you do not want any of the custom sources to take precedence over [PyPI](https://pypi.org),
you must declare **all** package sources to be [secondary](#secondary-package-sources).
If you prefer to disable PyPI completely,
you may choose to set one of your package sources to be the [default](#default-package-source)
or configure PyPI as [explicit source](#explicit-package-sources).
{{% /warning %}}
{{% /note %}}
#### Default Package Source
......@@ -164,6 +162,21 @@ poetry source add --priority=default foo https://foo.bar/simple/
{{% warning %}}
In a future version of Poetry, PyPI will be disabled automatically
if there is at least one custom source configured with another priority than `explicit`.
If you are using custom sources in addition to PyPI, you should configure PyPI explicitly
with a certain priority, e.g.
```bash
poetry source add --priority=primary PyPI
```
This way, the priority of PyPI can be set in a fine-granular way.
{{% /warning %}}
{{% warning %}}
Configuring a custom package source as default, will effectively disable [PyPI](https://pypi.org)
as a package source for your project.
......
......@@ -9,7 +9,7 @@ from poetry.repositories.repository_pool import Priority
@dataclasses.dataclass(order=True, eq=True)
class Source:
name: str
url: str
url: str = ""
default: dataclasses.InitVar[bool] = False
secondary: dataclasses.InitVar[bool] = False
priority: Priority = (
......@@ -38,6 +38,8 @@ class Source:
return dataclasses.asdict(
self,
dict_factory=lambda x: {
k: v if not isinstance(v, Priority) else v.name.lower() for (k, v) in x
k: v if not isinstance(v, Priority) else v.name.lower()
for (k, v) in x
if v
},
)
......@@ -19,7 +19,14 @@ class SourceAddCommand(Command):
"name",
"Source repository name.",
),
argument("url", "Source repository url."),
argument(
"url",
(
"Source repository URL."
" Required, except for PyPI, for which it is not allowed."
),
optional=True,
),
]
options = [
......@@ -57,10 +64,24 @@ class SourceAddCommand(Command):
from poetry.utils.source import source_to_table
name: str = self.argument("name")
lower_name = name.lower()
url: str = self.argument("url")
is_default: bool = self.option("default", False)
is_secondary: bool = self.option("secondary", False)
priority: Priority | None = self.option("priority", None)
priority_str: str | None = self.option("priority", None)
if lower_name == "pypi":
name = "PyPI"
if url:
self.line_error(
"<error>The URL of PyPI is fixed and cannot be set.</error>"
)
return 1
elif not url:
self.line_error(
"<error>A custom source cannot be added without a URL.</error>"
)
return 1
if is_default and is_secondary:
self.line_error(
......@@ -70,7 +91,7 @@ class SourceAddCommand(Command):
return 1
if is_default or is_secondary:
if priority is not None:
if priority_str is not None:
self.line_error(
"<error>Priority was passed through both --priority and a"
" deprecated flag (--default or --secondary). Please only provide"
......@@ -88,26 +109,17 @@ class SourceAddCommand(Command):
priority = Priority.DEFAULT
elif is_secondary:
priority = Priority.SECONDARY
elif priority is None:
elif priority_str is None:
priority = Priority.PRIMARY
new_source = Source(name=name, url=url, priority=priority)
existing_sources = self.poetry.get_sources()
else:
priority = Priority[priority_str.upper()]
sources = AoT([])
new_source = Source(name=name, url=url, priority=priority)
is_new_source = True
for source in existing_sources:
if source == new_source:
self.line(
f"Source with name <c1>{name}</c1> already exists. Skipping"
" addition."
)
return 0
elif (
source.priority is Priority.DEFAULT
and new_source.priority is Priority.DEFAULT
):
for source in self.poetry.get_sources():
if source.priority is Priority.DEFAULT and priority is Priority.DEFAULT:
self.line_error(
f"<error>Source with name <c1>{source.name}</c1> is already set to"
" default. Only one default source can be configured at a"
......@@ -115,7 +127,7 @@ class SourceAddCommand(Command):
)
return 1
if source.name == name:
if source.name.lower() == lower_name:
source = new_source
is_new_source = False
......
......@@ -21,12 +21,13 @@ class SourceRemoveCommand(Command):
from poetry.utils.source import source_to_table
name = self.argument("name")
lower_name = name.lower()
sources = AoT([])
removed = False
for source in self.poetry.get_sources():
if source.name == name:
if source.name.lower() == lower_name:
self.line(f"Removing source with name <c1>{source.name}</c1>.")
removed = True
continue
......
......@@ -27,12 +27,13 @@ class SourceShowCommand(Command):
def handle(self) -> int:
sources = self.poetry.get_sources()
names = self.argument("source")
lower_names = [name.lower() for name in names]
if not sources:
self.line("No sources configured for this project.")
return 0
if names and not any(s.name in names for s in sources):
if names and not any(s.name.lower() in lower_names for s in sources):
self.line_error(
f"No source found with name(s): {', '.join(names)}",
style="error",
......@@ -40,18 +41,14 @@ class SourceShowCommand(Command):
return 1
for source in sources:
if names and source.name not in names:
if names and source.name.lower() not in lower_names:
continue
table = self.table(style="compact")
rows: Rows = [
["<info>name</>", f" : <c1>{source.name}</>"],
["<info>url</>", f" : {source.url}"],
[
"<info>priority</>",
f" : {source.priority.name.lower()}",
],
]
rows: Rows = [["<info>name</>", f" : <c1>{source.name}</>"]]
if source.url:
rows.append(["<info>url</>", f" : {source.url}"])
rows.append(["<info>priority</>", f" : {source.priority.name.lower()}"])
table.add_rows(rows)
table.render()
self.line("")
......
......@@ -15,6 +15,7 @@ from poetry.core.packages.dependency_group import MAIN_GROUP
from poetry.core.packages.project_package import ProjectPackage
from poetry.config.config import Config
from poetry.exceptions import PoetryException
from poetry.json import validate_object
from poetry.packages.locker import Locker
from poetry.plugins.plugin import Plugin
......@@ -32,7 +33,7 @@ if TYPE_CHECKING:
from tomlkit.toml_document import TOMLDocument
from poetry.repositories import RepositoryPool
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.http_repository import HTTPRepository
from poetry.utils.dependency_specification import DependencySpec
logger = logging.getLogger(__name__)
......@@ -134,6 +135,7 @@ class Factory(BaseFactory):
pool = RepositoryPool()
explicit_pypi = False
for source in sources:
repository = cls.create_package_source(
source, auth_config, disable_cache=disable_cache
......@@ -163,14 +165,30 @@ class Factory(BaseFactory):
io.write_line(message)
pool.add_repository(repository, priority=priority)
if repository.name.lower() == "pypi":
explicit_pypi = True
# Only add PyPI if no default repository is configured
if not explicit_pypi:
if pool.has_default():
if io.is_debug():
io.write_line("Deactivating the PyPI repository")
else:
from poetry.repositories.pypi_repository import PyPiRepository
if pool.repositories:
io.write_error_line(
"<warning>"
"Warning: In a future version of Poetry, PyPI will be disabled"
" automatically if at least one custom source is configured"
" with another priority than 'explicit'. In order to avoid"
" a breaking change and make your pyproject.toml forward"
" compatible, add PyPI explicitly via 'poetry source add pypi'."
" By the way, this has the advantage that you can set the"
" priority of PyPI as with any other source."
"</warning>"
)
if pool.has_primary_repositories():
pypi_priority = Priority.SECONDARY
else:
......@@ -180,23 +198,38 @@ class Factory(BaseFactory):
PyPiRepository(disable_cache=disable_cache), priority=pypi_priority
)
if not pool.repositories:
raise PoetryException(
"At least one source must not be configured as 'explicit'."
)
return pool
@classmethod
def create_package_source(
cls, source: dict[str, str], auth_config: Config, disable_cache: bool = False
) -> LegacyRepository:
) -> HTTPRepository:
from poetry.repositories.exceptions import InvalidSourceError
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.repositories.single_page_repository import SinglePageRepository
if "url" not in source:
raise RuntimeError("Unsupported source specified")
# PyPI-like repository
if "name" not in source:
raise RuntimeError("Missing [name] in source.")
try:
name = source["name"]
except KeyError:
raise InvalidSourceError("Missing [name] in source.")
if name.lower() == "pypi":
if "url" in source:
raise InvalidSourceError(
"The PyPI repository cannot be configured with a custom url."
)
return PyPiRepository(disable_cache=disable_cache)
try:
url = source["url"]
except KeyError:
raise InvalidSourceError(f"Missing [url] in source {name!r}.")
repository_class = LegacyRepository
......
......@@ -20,8 +20,7 @@
"type": "object",
"additionalProperties": false,
"required": [
"name",
"url"
"name"
],
"properties": {
"name": {
......
......@@ -7,3 +7,7 @@ class RepositoryError(Exception):
class PackageNotFound(Exception):
pass
class InvalidSourceError(Exception):
pass
......@@ -58,6 +58,16 @@ def source_explicit() -> Source:
)
@pytest.fixture
def source_pypi() -> Source:
return Source(name="PyPI")
@pytest.fixture
def source_pypi_explicit() -> Source:
return Source(name="PyPI", priority=Priority.EXPLICIT)
_existing_source = Source(name="existing", url="https://existing.com")
......@@ -88,6 +98,20 @@ url = "{_existing_source.url}"
"""
PYPROJECT_WITH_PYPI = f"""{PYPROJECT_WITHOUT_SOURCES}
[[tool.poetry.source]]
name = "PyPI"
"""
PYPROJECT_WITH_PYPI_AND_OTHER = f"""{PYPROJECT_WITH_SOURCES}
[[tool.poetry.source]]
name = "PyPI"
"""
@pytest.fixture
def poetry_without_source(project_factory: ProjectFactory) -> Poetry:
return project_factory(pyproject_content=PYPROJECT_WITHOUT_SOURCES)
......@@ -99,6 +123,16 @@ def poetry_with_source(project_factory: ProjectFactory) -> Poetry:
@pytest.fixture
def poetry_with_pypi(project_factory: ProjectFactory) -> Poetry:
return project_factory(pyproject_content=PYPROJECT_WITH_PYPI)
@pytest.fixture
def poetry_with_pypi_and_other(project_factory: ProjectFactory) -> Poetry:
return project_factory(pyproject_content=PYPROJECT_WITH_PYPI_AND_OTHER)
@pytest.fixture
def add_multiple_sources(
command_tester_factory: CommandTesterFactory,
poetry_with_source: Poetry,
......
......@@ -166,16 +166,47 @@ def test_source_add_error_priority_and_deprecated_legacy(tester: CommandTester)
assert tester.status_code == 1
def test_source_add_error_no_url(tester: CommandTester) -> None:
tester.execute("foo")
assert (
tester.io.fetch_error().strip()
== "A custom source cannot be added without a URL."
)
assert tester.status_code == 1
def test_source_add_error_pypi(tester: CommandTester) -> None:
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"
tester.io.fetch_error().strip() == "The URL of PyPI is fixed and cannot be set."
)
assert tester.status_code == 1
@pytest.mark.parametrize("name", ["pypi", "PyPI"])
def test_source_add_pypi(
name: str,
tester: CommandTester,
source_existing: Source,
source_pypi: Source,
poetry_with_source: Poetry,
) -> None:
tester.execute(name)
assert_source_added(tester, poetry_with_source, source_existing, source_pypi)
def test_source_add_pypi_explicit(
tester: CommandTester,
source_existing: Source,
source_pypi_explicit: Source,
poetry_with_source: Poetry,
) -> None:
tester.execute("--priority=explicit PyPI")
assert_source_added(
tester, poetry_with_source, source_existing, source_pypi_explicit
)
def test_source_add_existing_legacy(
tester: CommandTester, source_existing: Source, poetry_with_source: Poetry
) -> None:
......@@ -202,29 +233,41 @@ def test_source_add_existing_legacy(
assert sources[0] == expected_source
def test_source_add_existing_no_change(
tester: CommandTester, source_existing: Source, poetry_with_source: Poetry
@pytest.mark.parametrize("modifier", ["lower", "upper"])
def test_source_add_existing_no_change_except_case_of_name(
modifier: str,
tester: CommandTester,
source_existing: Source,
poetry_with_source: Poetry,
) -> None:
tester.execute(f"--priority=primary {source_existing.name} {source_existing.url}")
name = getattr(source_existing.name, modifier)()
tester.execute(f"--priority=primary {name} {source_existing.url}")
assert (
tester.io.fetch_output().strip()
== f"Source with name {source_existing.name} already exists. Skipping addition."
== f"Source with name {name} already exists. Updating."
)
poetry_with_source.pyproject.reload()
sources = poetry_with_source.get_sources()
assert len(sources) == 1
assert sources[0] == source_existing
assert sources[0].name == getattr(source_existing.name, modifier)()
assert sources[0].url == source_existing.url
assert sources[0].priority == source_existing.priority
@pytest.mark.parametrize("modifier", ["lower", "upper"])
def test_source_add_existing_updating(
tester: CommandTester, source_existing: Source, poetry_with_source: Poetry
modifier: str,
tester: CommandTester,
source_existing: Source,
poetry_with_source: Poetry,
) -> None:
tester.execute(f"--priority=default {source_existing.name} {source_existing.url}")
name = getattr(source_existing.name, modifier)()
tester.execute(f"--priority=default {name} {source_existing.url}")
assert (
tester.io.fetch_output().strip()
== f"Source with name {source_existing.name} already exists. Updating."
== f"Source with name {name} already exists. Updating."
)
poetry_with_source.pyproject.reload()
......@@ -233,12 +276,14 @@ def test_source_add_existing_updating(
assert len(sources) == 1
assert sources[0] != source_existing
expected_source = Source(
name=source_existing.name, url=source_existing.url, priority=Priority.DEFAULT
name=name, url=source_existing.url, priority=Priority.DEFAULT
)
assert sources[0] == expected_source
@pytest.mark.parametrize("modifier", ["lower", "upper"])
def test_source_add_existing_fails_due_to_other_default(
modifier: str,
tester: CommandTester,
source_existing: Source,
source_default: Source,
......@@ -247,7 +292,8 @@ def test_source_add_existing_fails_due_to_other_default(
tester.execute(f"--priority=default {source_default.name} {source_default.url}")
tester.io.fetch_output()
tester.execute(f"--priority=default {source_existing.name} {source_existing.url}")
name = getattr(source_existing.name, modifier)()
tester.execute(f"--priority=default {name} {source_existing.url}")
assert (
tester.io.fetch_error().strip()
......
......@@ -22,14 +22,32 @@ def tester(
return command_tester_factory("source remove", poetry=poetry_with_source)
@pytest.fixture
def tester_pypi(
command_tester_factory: CommandTesterFactory,
poetry_with_pypi: Poetry,
) -> CommandTester:
return command_tester_factory("source remove", poetry=poetry_with_pypi)
@pytest.fixture
def tester_pypi_and_other(
command_tester_factory: CommandTesterFactory,
poetry_with_pypi_and_other: Poetry,
) -> CommandTester:
return command_tester_factory("source remove", poetry=poetry_with_pypi_and_other)
@pytest.mark.parametrize("modifier", ["lower", "upper"])
def test_source_remove_simple(
tester: CommandTester,
poetry_with_source: Poetry,
source_existing: Source,
source_one: Source,
source_two: Source,
modifier: str,
) -> None:
tester.execute(f"{source_existing.name}")
tester.execute(getattr(f"{source_existing.name}", modifier)())
assert (
tester.io.fetch_output().strip()
== f"Removing source with name {source_existing.name}."
......@@ -42,7 +60,42 @@ def test_source_remove_simple(
assert tester.status_code == 0
def test_source_remove_error(tester: CommandTester) -> None:
tester.execute("error")
assert tester.io.fetch_error().strip() == "Source with name error was not found."
@pytest.mark.parametrize("name", ["pypi", "PyPI"])
def test_source_remove_pypi(
name: str, tester_pypi: CommandTester, poetry_with_pypi: Poetry
) -> None:
tester_pypi.execute(name)
assert tester_pypi.io.fetch_output().strip() == "Removing source with name PyPI."
poetry_with_pypi.pyproject.reload()
sources = poetry_with_pypi.get_sources()
assert sources == []
assert tester_pypi.status_code == 0
@pytest.mark.parametrize("name", ["pypi", "PyPI"])
def test_source_remove_pypi_and_other(
name: str,
tester_pypi_and_other: CommandTester,
poetry_with_pypi_and_other: Poetry,
source_existing: Source,
) -> None:
tester_pypi_and_other.execute(name)
assert (
tester_pypi_and_other.io.fetch_output().strip()
== "Removing source with name PyPI."
)
poetry_with_pypi_and_other.pyproject.reload()
sources = poetry_with_pypi_and_other.get_sources()
assert sources == [source_existing]
assert tester_pypi_and_other.status_code == 0
@pytest.mark.parametrize("name", ["foo", "pypi", "PyPI"])
def test_source_remove_error(name: str, tester: CommandTester) -> None:
tester.execute(name)
assert tester.io.fetch_error().strip() == f"Source with name {name} was not found."
assert tester.status_code == 1
......@@ -31,6 +31,22 @@ def tester_no_sources(
@pytest.fixture
def tester_pypi(
command_tester_factory: CommandTesterFactory,
poetry_with_pypi: Poetry,
) -> CommandTester:
return command_tester_factory("source show", poetry=poetry_with_pypi)
@pytest.fixture
def tester_pypi_and_other(
command_tester_factory: CommandTesterFactory,
poetry_with_pypi_and_other: Poetry,
) -> CommandTester:
return command_tester_factory("source show", poetry=poetry_with_pypi_and_other)
@pytest.fixture
def tester_all_types(
command_tester_factory: CommandTesterFactory,
poetry_with_source: Poetry,
......@@ -61,8 +77,11 @@ priority : primary
assert tester.status_code == 0
def test_source_show_one(tester: CommandTester, source_one: Source) -> None:
tester.execute(f"{source_one.name}")
@pytest.mark.parametrize("modifier", ["lower", "upper"])
def test_source_show_one(
tester: CommandTester, source_one: Source, modifier: str
) -> None:
tester.execute(getattr(f"{source_one.name}", modifier)())
expected = """\
name : one
......@@ -75,10 +94,11 @@ priority : primary
assert tester.status_code == 0
@pytest.mark.parametrize("modifier", ["lower", "upper"])
def test_source_show_two(
tester: CommandTester, source_one: Source, source_two: Source
tester: CommandTester, source_one: Source, source_two: Source, modifier: str
) -> None:
tester.execute(f"{source_one.name} {source_two.name}")
tester.execute(getattr(f"{source_one.name} {source_two.name}", modifier)())
expected = """\
name : one
......@@ -121,6 +141,35 @@ priority : {source.name}
assert tester_all_types.status_code == 0
def test_source_show_pypi(tester_pypi: CommandTester) -> None:
tester_pypi.execute("")
expected = """\
name : PyPI
priority : primary
""".splitlines()
assert [
line.strip() for line in tester_pypi.io.fetch_output().strip().splitlines()
] == expected
assert tester_pypi.status_code == 0
def test_source_show_pypi_and_other(tester_pypi_and_other: CommandTester) -> None:
tester_pypi_and_other.execute("")
expected = """\
name : existing
url : https://existing.com
priority : primary
name : PyPI
priority : primary
""".splitlines()
assert [
line.strip()
for line in tester_pypi_and_other.io.fetch_output().strip().splitlines()
] == expected
assert tester_pypi_and_other.status_code == 0
def test_source_show_no_sources(tester_no_sources: CommandTester) -> None:
tester_no_sources.execute("error")
assert (
......
......@@ -22,11 +22,11 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
requests = { version = "^2.18", optional = true, extras=[ "security" ] }
pathlib2 = { version = "^2.2", python = "~2.7" }
pathlib2 = { version = "^2.2", python = "~3.6" }
orator = { version = "^0.9", optional = true }
......
[tool.poetry]
name = "with-default-source-and-pypi"
version = "1.2.3"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
readme = "README.rst"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
# Requirements
[tool.poetry.dependencies]
python = "^3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
requests = { version = "^2.18", optional = true, extras=[ "security" ] }
pathlib2 = { version = "^2.2", python = "~3.6" }
orator = { version = "^0.9", optional = true }
# File dependency
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }
# Dir dependency with setup.py
my-package = { path = "../project_with_setup/" }
# Dir dependency with pyproject.toml
simple-project = { path = "../simple_project/" }
[tool.poetry.extras]
db = [ "orator" ]
[tool.poetry.dev-dependencies]
pytest = "~3.4"
[tool.poetry.scripts]
my-script = "my_package:main"
[tool.poetry.plugins."blogtool.parsers"]
".rst" = "some_module::SomeClass"
[[tool.poetry.source]]
name = "foo"
url = "https://foo.bar/simple/"
priority = "default"
[[tool.poetry.source]]
name = "PyPI"
......@@ -22,11 +22,11 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
requests = { version = "^2.18", optional = true, extras=[ "security" ] }
pathlib2 = { version = "^2.2", python = "~2.7" }
pathlib2 = { version = "^2.2", python = "~3.6" }
orator = { version = "^0.9", optional = true }
......
[tool.poetry]
name = "with-default-source-pypi"
version = "1.2.3"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
readme = "README.rst"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
# Requirements
[tool.poetry.dependencies]
python = "^3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
requests = { version = "^2.18", optional = true, extras=[ "security" ] }
pathlib2 = { version = "^2.2", python = "~3.6" }
orator = { version = "^0.9", optional = true }
# File dependency
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }
# Dir dependency with setup.py
my-package = { path = "../project_with_setup/" }
# Dir dependency with pyproject.toml
simple-project = { path = "../simple_project/" }
[tool.poetry.extras]
db = [ "orator" ]
[tool.poetry.dev-dependencies]
pytest = "~3.4"
[tool.poetry.scripts]
my-script = "my_package:main"
[tool.poetry.plugins."blogtool.parsers"]
".rst" = "some_module::SomeClass"
[[tool.poetry.source]]
name = "PyPI"
priority = "default"
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Your Name <you@example.com>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "^3.6"
[tool.poetry.dev-dependencies]
[[tool.poetry.source]]
name = "foo"
url = "https://foo.bar/simple/"
[[tool.poetry.source]]
name = "PyPI"
priority = "explicit"
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Your Name <you@example.com>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "^3.6"
[tool.poetry.dev-dependencies]
[[tool.poetry.source]]
name = "explicit"
url = "https://explicit.com/simple/"
priority = "explicit"
[[tool.poetry.source]]
name = "PyPI"
priority = "explicit"
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Your Name <you@example.com>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "^3.6"
[tool.poetry.dev-dependencies]
[[tool.poetry.source]]
name = "PyPI"
priority = "explicit"
......@@ -9,7 +9,7 @@ license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
[tool.poetry.dev-dependencies]
......
......@@ -9,7 +9,7 @@ license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
[tool.poetry.dev-dependencies]
......
......@@ -9,7 +9,7 @@ license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
[tool.poetry.dev-dependencies]
......
......@@ -9,7 +9,7 @@ license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
[tool.poetry.dev-dependencies]
......
......@@ -9,7 +9,7 @@ license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
[tool.poetry.dev-dependencies]
......
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Your Name <you@example.com>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "^3.6"
[tool.poetry.dev-dependencies]
[[tool.poetry.source]]
name = "foo"
url = "https://foo.bar/simple/"
priority = "secondary"
[[tool.poetry.source]]
name = "bar"
url = "https://bar.baz/simple/"
[[tool.poetry.source]]
name = "PyPI"
[[tool.poetry.source]]
name = "baz"
url = "https://baz.bar/simple/"
......@@ -9,7 +9,7 @@ license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
[tool.poetry.dev-dependencies]
......
......@@ -9,7 +9,7 @@ license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
[tool.poetry.dev-dependencies]
......
......@@ -9,7 +9,7 @@ license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
[tool.poetry.dev-dependencies]
......
......@@ -9,7 +9,7 @@ license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
[tool.poetry.dev-dependencies]
......
......@@ -22,11 +22,11 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
requests = { version = "^2.18", optional = true, extras=[ "security" ] }
pathlib2 = { version = "^2.2", python = "~2.7" }
pathlib2 = { version = "^2.2", python = "~3.6" }
orator = { version = "^0.9", optional = true }
......
......@@ -22,11 +22,11 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "^3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
requests = { version = "^2.18", optional = true, extras=[ "security" ] }
pathlib2 = { version = "^2.2", python = "~2.7" }
pathlib2 = { version = "^2.2", python = "~3.6" }
orator = { version = "^0.9", optional = true }
......
......@@ -21,15 +21,6 @@ def test_pyproject_toml_valid() -> None:
assert Factory.validate(content) == {"errors": [], "warnings": []}
def test_pyproject_toml_invalid_url() -> None:
toml = TOMLFile(FIXTURE_DIR / "complete_invalid_url.toml").read()
content = toml["tool"]["poetry"]
assert Factory.validate(content) == {
"errors": ["[source.0] 'url' is a required property"],
"warnings": [],
}
def test_pyproject_toml_invalid_priority() -> None:
toml = TOMLFile(FIXTURE_DIR / "complete_invalid_priority.toml").read()
content = toml["tool"]["poetry"]
......
......@@ -9,8 +9,10 @@ from deepdiff import DeepDiff
from packaging.utils import canonicalize_name
from poetry.core.constraints.version import parse_constraint
from poetry.exceptions import PoetryException
from poetry.factory import Factory
from poetry.plugins.plugin import Plugin
from poetry.repositories.exceptions import InvalidSourceError
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.repositories.repository_pool import Priority
......@@ -22,6 +24,7 @@ if TYPE_CHECKING:
from cleo.io.io import IO
from pytest_mock import MockerFixture
from poetry.config.config import Config
from poetry.poetry import Poetry
from tests.types import FixtureDirGetter
......@@ -231,6 +234,31 @@ def test_poetry_with_default_source(
assert io.fetch_error() == ""
def test_poetry_with_default_source_and_pypi(
fixture_dir: FixtureDirGetter, with_simple_keyring: None
) -> None:
io = BufferedIO()
poetry = Factory().create_poetry(fixture_dir("with_default_source_and_pypi"), io=io)
assert len(poetry.pool.repositories) == 2
assert poetry.pool.has_repository("PyPI")
assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY
assert "Warning: Found deprecated key" not in io.fetch_error()
def test_poetry_with_default_source_pypi(
fixture_dir: FixtureDirGetter, with_simple_keyring: None
) -> None:
io = BufferedIO()
poetry = Factory().create_poetry(fixture_dir("with_default_source_pypi"), io=io)
assert len(poetry.pool.repositories) == 1
assert poetry.pool.has_repository("PyPI")
assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT
@pytest.mark.parametrize(
"project",
("with_non_default_source_implicit", "with_non_default_source_explicit"),
......@@ -238,7 +266,8 @@ def test_poetry_with_default_source(
def test_poetry_with_non_default_source(
project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None
) -> None:
poetry = Factory().create_poetry(fixture_dir(project))
io = BufferedIO()
poetry = Factory().create_poetry(fixture_dir(project), io=io)
assert not poetry.pool.has_default()
assert poetry.pool.has_repository("PyPI")
......@@ -248,6 +277,8 @@ def test_poetry_with_non_default_source(
assert poetry.pool.get_priority("foo") is Priority.PRIMARY
assert isinstance(poetry.pool.repository("foo"), LegacyRepository)
assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo"}
error = io.fetch_error()
assert "Warning: In a future version of Poetry, PyPI will be disabled" in error
def test_poetry_with_non_default_secondary_source_legacy(
......@@ -347,6 +378,26 @@ def test_poetry_with_non_default_multiple_sources(
assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "bar", "foo"}
def test_poetry_with_non_default_multiple_sources_pypi(
fixture_dir: FixtureDirGetter, with_simple_keyring: None
) -> None:
io = BufferedIO()
poetry = Factory().create_poetry(
fixture_dir("with_non_default_multiple_sources_pypi"), io=io
)
assert len(poetry.pool.repositories) == 4
assert not poetry.pool.has_default()
assert poetry.pool.has_repository("PyPI")
assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY
# PyPI must be between bar and baz!
expected = ["bar", "PyPI", "baz", "foo"]
assert [repo.name for repo in poetry.pool.repositories] == expected
error = io.fetch_error()
assert error == ""
def test_poetry_with_no_default_source(fixture_dir: FixtureDirGetter) -> None:
poetry = Factory().create_poetry(fixture_dir("sample_project"))
......@@ -371,6 +422,29 @@ def test_poetry_with_explicit_source(
assert [repo.name for repo in poetry.pool.repositories] == ["PyPI"]
def test_poetry_with_explicit_pypi_and_other(
fixture_dir: FixtureDirGetter, with_simple_keyring: None
) -> None:
io = BufferedIO()
poetry = Factory().create_poetry(fixture_dir("with_explicit_pypi_and_other"), io=io)
assert len(poetry.pool.repositories) == 1
assert len(poetry.pool.all_repositories) == 2
error = io.fetch_error()
assert error == ""
@pytest.mark.parametrize(
"project", ["with_explicit_pypi_no_other", "with_explicit_pypi_and_other_explicit"]
)
def test_poetry_with_pypi_explicit_only(
project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None
) -> None:
with pytest.raises(PoetryException) as e:
Factory().create_poetry(fixture_dir(project))
assert str(e.value) == "At least one source must not be configured as 'explicit'."
def test_poetry_with_two_default_sources_legacy(
fixture_dir: FixtureDirGetter, with_simple_keyring: None
) -> None:
......@@ -442,3 +516,27 @@ def test_create_poetry_with_plugins(
poetry = Factory().create_poetry(fixture_dir("sample_project"))
assert poetry.package.readmes == ("README.md",)
@pytest.mark.parametrize(
("source", "expected"),
[
({}, "Missing [name] in source."),
({"name": "foo"}, "Missing [url] in source 'foo'."),
(
{"name": "PyPI", "url": "https://example.com"},
"The PyPI repository cannot be configured with a custom url.",
),
],
)
def test_create_package_source_invalid(
source: dict[str, str],
expected: str,
config: Config,
fixture_dir: FixtureDirGetter,
) -> None:
with pytest.raises(InvalidSourceError) as e:
Factory.create_package_source(source, auth_config=config)
Factory().create_poetry(fixture_dir("with_source_pypi_url"))
assert str(e.value) == expected
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