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

Python version confusion #457

Merged
merged 3 commits into from
Sep 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 4 additions & 89 deletions src/poetry/core/packages/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from poetry.core.packages.dependency_group import MAIN_GROUP
from poetry.core.packages.specification import PackageSpecification
from poetry.core.packages.utils.utils import contains_group_without_marker
from poetry.core.packages.utils.utils import create_nested_marker
from poetry.core.packages.utils.utils import normalize_python_version_markers
from poetry.core.semver.helpers import parse_constraint
from poetry.core.semver.version_range_constraint import VersionRangeConstraint
Expand All @@ -25,7 +26,6 @@
if TYPE_CHECKING:
from packaging.utils import NormalizedName

from poetry.core.packages.constraints import BaseConstraint
from poetry.core.packages.directory_dependency import DirectoryDependency
from poetry.core.packages.file_dependency import FileDependency
from poetry.core.semver.version_constraint import VersionConstraint
Expand Down Expand Up @@ -149,9 +149,7 @@ def python_versions(self, value: str) -> None:
if not self._python_constraint.is_any():
self._marker = self._marker.intersect(
parse_marker(
self._create_nested_marker(
"python_version", self._python_constraint
)
create_nested_marker("python_version", self._python_constraint)
)
)

Expand Down Expand Up @@ -312,13 +310,13 @@ def to_pep_508(self, with_extras: bool = True) -> str:
python_constraint = self.python_constraint

markers.append(
self._create_nested_marker("python_version", python_constraint)
create_nested_marker("python_version", python_constraint)
)

in_extras = " || ".join(self._in_extras)
if in_extras and with_extras and not has_extras:
markers.append(
self._create_nested_marker("extra", parse_generic_constraint(in_extras))
create_nested_marker("extra", parse_generic_constraint(in_extras))
)

if markers:
Expand All @@ -333,89 +331,6 @@ def to_pep_508(self, with_extras: bool = True) -> str:

return requirement

def _create_nested_marker(
self, name: str, constraint: BaseConstraint | VersionConstraint
) -> str:
from poetry.core.packages.constraints.constraint import Constraint
from poetry.core.packages.constraints.multi_constraint import MultiConstraint
from poetry.core.packages.constraints.union_constraint import UnionConstraint
from poetry.core.semver.version import Version
from poetry.core.semver.version_union import VersionUnion

if isinstance(constraint, (MultiConstraint, UnionConstraint)):
multi_parts = []
for c in constraint.constraints:
multi = isinstance(c, (MultiConstraint, UnionConstraint))
multi_parts.append((multi, self._create_nested_marker(name, c)))

glue = " and "
if isinstance(constraint, UnionConstraint):
parts = [f"({part[1]})" if part[0] else part[1] for part in multi_parts]
glue = " or "
else:
parts = [part[1] for part in multi_parts]

marker = glue.join(parts)
elif isinstance(constraint, Constraint):
marker = f'{name} {constraint.operator} "{constraint.version}"'
elif isinstance(constraint, VersionUnion):
parts = [self._create_nested_marker(name, c) for c in constraint.ranges]
glue = " or "
parts = [f"({part})" for part in parts]

marker = glue.join(parts)
elif isinstance(constraint, Version):
if constraint.precision >= 3 and name == "python_version":
name = "python_full_version"

marker = f'{name} == "{constraint.text}"'
else:
assert isinstance(constraint, VersionRangeConstraint)
if constraint.min is not None:
min_name = name
if constraint.min.precision >= 3 and name == "python_version":
min_name = "python_full_version"

if constraint.max is None:
name = min_name

op = ">="
if not constraint.include_min:
op = ">"

version = constraint.min.text
if constraint.max is not None:
max_name = name
if constraint.max.precision >= 3 and name == "python_version":
max_name = "python_full_version"

text = f'{min_name} {op} "{version}"'

op = "<="
if not constraint.include_max:
op = "<"

version = constraint.max.text

text += f' and {max_name} {op} "{version}"'

return text
elif constraint.max is not None:
if constraint.max.precision >= 3 and name == "python_version":
name = "python_full_version"

op = "<="
if not constraint.include_max:
op = "<"

version = constraint.max.text
else:
return ""

marker = f'{name} {op} "{version}"'

return marker

def activate(self) -> None:
"""
Set the dependency as mandatory.
Expand Down
6 changes: 2 additions & 4 deletions src/poetry/core/packages/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from poetry.core.packages.dependency_group import MAIN_GROUP
from poetry.core.packages.specification import PackageSpecification
from poetry.core.packages.utils.utils import create_nested_marker
from poetry.core.packages.utils.utils import get_python_constraint_from_marker
from poetry.core.semver.helpers import parse_constraint
from poetry.core.version.markers import parse_marker

Expand Down Expand Up @@ -263,11 +262,10 @@ def python_versions(self) -> str:
@python_versions.setter
def python_versions(self, value: str) -> None:
self._python_versions = value
constraint = parse_constraint(value)
self._python_constraint = parse_constraint(value)
self._python_marker = parse_marker(
create_nested_marker("python_version", constraint)
create_nested_marker("python_version", self._python_constraint)
)
self._python_constraint = get_python_constraint_from_marker(self._python_marker)

@property
def python_constraint(self) -> VersionConstraint:
Expand Down
86 changes: 49 additions & 37 deletions src/poetry/core/packages/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
if TYPE_CHECKING:
from poetry.core.packages.constraints import BaseConstraint
from poetry.core.semver.version_constraint import VersionConstraint
from poetry.core.semver.version_union import VersionUnion
from poetry.core.version.markers import BaseMarker

# Even though we've `from __future__ import annotations`, mypy doesn't seem to like
Expand Down Expand Up @@ -202,7 +201,7 @@ def contains_group_without_marker(markers: ConvertedMarkers, marker_name: str) -

def create_nested_marker(
name: str,
constraint: BaseConstraint | VersionUnion | Version | VersionConstraint,
constraint: BaseConstraint | VersionConstraint,
) -> str:
from poetry.core.packages.constraints.constraint import Constraint
from poetry.core.packages.constraints.multi_constraint import MultiConstraint
Expand Down Expand Up @@ -240,44 +239,57 @@ def create_nested_marker(
marker = f'{name} == "{constraint.text}"'
else:
assert isinstance(constraint, VersionRange)
min_name = max_name = name

parts = []

# `python_version` is a special case: to keep the constructed marker equivalent
# to the constraint we need to be careful with the precision.
#
# PEP 440 tells us that when we come to make the comparison the release
# segment will be zero padded: eg "<= 3.10" is equivalent to "<= 3.10.0".
#
# But "python_version <= 3.10" is _not_ equivalent to "python_version <= 3.10.0"
# - see normalize_python_version_markers.
#
# A similar issue arises for a constraint like "> 3.6".
if constraint.min is not None:
op = ">="
if not constraint.include_min:
op = ">"

op = ">=" if constraint.include_min else ">"
version = constraint.min
if constraint.max is not None:
min_name = max_name = name
if min_name == "python_version" and constraint.min.precision >= 3:
min_name = "python_full_version"

if max_name == "python_version" and constraint.max.precision >= 3:
max_name = "python_full_version"

text = f'{min_name} {op} "{version}"'

op = "<="
if not constraint.include_max:
op = "<"

version = constraint.max

text += f' and {max_name} {op} "{version}"'

return text
elif constraint.max is not None:
op = "<="
if not constraint.include_max:
op = "<"

if min_name == "python_version" and version.precision >= 3:
min_name = "python_full_version"

if (
min_name == "python_version"
and not constraint.include_min
and version.precision < 3
):
padding = ".0" * (3 - version.precision)
part = f'python_full_version > "{version}{padding}"'
else:
part = f'{min_name} {op} "{version}"'

parts.append(part)

if constraint.max is not None:
op = "<=" if constraint.include_max else "<"
version = constraint.max
else:
return ""

if name == "python_version" and version.precision >= 3:
name = "python_full_version"

marker = f'{name} {op} "{version}"'
if max_name == "python_version" and version.precision >= 3:
max_name = "python_full_version"

if (
max_name == "python_version"
and constraint.include_max
and version.precision < 3
):
padding = ".0" * (3 - version.precision)
part = f'python_full_version <= "{version}{padding}"'
else:
part = f'{max_name} {op} "{version}"'

parts.append(part)

marker = " and ".join(parts)

return marker

Expand Down
6 changes: 3 additions & 3 deletions tests/packages/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,15 +533,15 @@ def test_package_pep592_yanked(
assert package.yanked_reason == expected_yanked_reason


def test_python_versions_are_normalized() -> None:
def test_python_versions_are_made_precise() -> None:
package = Package("foo", "1.2.3")
package.python_versions = ">3.6,<=3.10"

assert (
str(package.python_marker)
== 'python_version > "3.6" and python_version <= "3.10"'
== 'python_full_version > "3.6.0" and python_full_version <= "3.10.0"'
)
assert str(package.python_constraint) == ">=3.7,<3.11"
assert str(package.python_constraint) == ">3.6,<=3.10"


def test_cannot_update_package_version() -> None:
Expand Down
78 changes: 75 additions & 3 deletions tests/packages/utils/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

import pytest

from poetry.core.packages.constraints import parse_constraint
from poetry.core.packages.utils.utils import convert_markers
from poetry.core.packages.utils.utils import create_nested_marker
from poetry.core.packages.utils.utils import get_python_constraint_from_marker
from poetry.core.packages.utils.utils import is_python_project
from poetry.core.semver.helpers import parse_constraint
from poetry.core.semver.helpers import parse_constraint as parse_version_constraint
from poetry.core.version.markers import parse_marker


Expand Down Expand Up @@ -78,6 +80,76 @@ def test_convert_markers(
assert converted == expected


@pytest.mark.parametrize(
["constraint", "expected"],
[
("*", ""),
("==linux", 'sys_platform == "linux"'),
("!=win32", 'sys_platform != "win32"'),
("!=linux, !=win32", 'sys_platform != "linux" and sys_platform != "win32"'),
("==linux || ==win32", 'sys_platform == "linux" or sys_platform == "win32"'),
],
)
def test_create_nested_marker_base_constraint(constraint: str, expected: str) -> None:
assert (
create_nested_marker("sys_platform", parse_constraint(constraint)) == expected
)


@pytest.mark.parametrize(
["constraint", "expected"],
[
("*", ""),
# simple version
("3", 'python_version == "3"'),
("3.9", 'python_version == "3.9"'),
("3.9.0", 'python_full_version == "3.9.0"'),
("3.9.1", 'python_full_version == "3.9.1"'),
# min
(">=3", 'python_version >= "3"'),
(">=3.9", 'python_version >= "3.9"'),
(">=3.9.0", 'python_full_version >= "3.9.0"'),
(">=3.9.1", 'python_full_version >= "3.9.1"'),
(">3", 'python_full_version > "3.0.0"'),
(">3.9", 'python_full_version > "3.9.0"'),
(">3.9.0", 'python_full_version > "3.9.0"'),
(">3.9.1", 'python_full_version > "3.9.1"'),
# max
("<3", 'python_version < "3"'),
("<3.9", 'python_version < "3.9"'),
("<3.9.0", 'python_full_version < "3.9.0"'),
("<3.9.1", 'python_full_version < "3.9.1"'),
("<=3", 'python_full_version <= "3.0.0"'),
("<=3.9", 'python_full_version <= "3.9.0"'),
("<=3.9.0", 'python_full_version <= "3.9.0"'),
("<=3.9.1", 'python_full_version <= "3.9.1"'),
# min and max
(">=3.7, <3.9", 'python_version >= "3.7" and python_version < "3.9"'),
(">=3.7, <=3.9", 'python_version >= "3.7" and python_full_version <= "3.9.0"'),
(">3.7, <3.9", 'python_full_version > "3.7.0" and python_version < "3.9"'),
(
">3.7, <=3.9",
'python_full_version > "3.7.0" and python_full_version <= "3.9.0"',
),
# union
("<3.7 || >=3.8", '(python_version < "3.7") or (python_version >= "3.8")'),
(
">=3.7,<3.8 || >=3.9,<=3.10",
'(python_version >= "3.7" and python_version < "3.8")'
' or (python_version >= "3.9" and python_full_version <= "3.10.0")',
),
],
)
def test_create_nested_marker_version_constraint(
constraint: str,
expected: str,
) -> None:
assert (
create_nested_marker("python_version", parse_version_constraint(constraint))
== expected
)


@pytest.mark.parametrize(
["marker", "constraint"],
[
Expand Down Expand Up @@ -143,7 +215,7 @@ def test_convert_markers(
)
def test_get_python_constraint_from_marker(marker: str, constraint: str) -> None:
marker_parsed = parse_marker(marker)
constraint_parsed = parse_constraint(constraint)
constraint_parsed = parse_version_constraint(constraint)
assert get_python_constraint_from_marker(marker_parsed) == constraint_parsed


Expand All @@ -158,6 +230,6 @@ def test_get_python_constraint_from_marker(marker: str, constraint: str) -> None
("does_not_exist", False),
],
)
def test_package_utils_is_python_project(fixture: str, result: bool) -> None:
def test_is_python_project(fixture: str, result: bool) -> None:
path = Path(__file__).parent.parent.parent / "fixtures" / fixture
assert is_python_project(path) == result