Skip to content

Commit f52e9cf

Browse files
committed
pylock select: make it a Pylock instance method
1 parent 77ce285 commit f52e9cf

File tree

2 files changed

+200
-171
lines changed

2 files changed

+200
-171
lines changed

src/packaging/pylock.py

Lines changed: 172 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@ class PylockUnsupportedVersionError(PylockValidationError):
273273
"""Raised when encountering an unsupported `lock_version`."""
274274

275275

276+
class PylockSelectError(Exception):
277+
"""Base exception for errors raised by :method:`Pylock.select()`."""
278+
279+
276280
@dataclass(frozen=True, init=False)
277281
class PackageVcs:
278282
type: str
@@ -638,186 +642,188 @@ def validate(self) -> None:
638642
Raises :class:`PylockValidationError` otherwise."""
639643
self.from_dict(self.to_dict())
640644

645+
def select(
646+
self,
647+
*,
648+
environment: Environment | None = None,
649+
tags: Sequence[Tag] | None = None,
650+
extras: Collection[str] | None = None,
651+
dependency_groups: Collection[str] | None = None,
652+
) -> Iterator[ # XXX or Iterable?
653+
tuple[
654+
Package,
655+
PackageVcs
656+
| PackageDirectory
657+
| PackageArchive
658+
| PackageWheel
659+
| PackageSdist,
660+
]
661+
]:
662+
"""Select what to install from the lock file.
663+
664+
The *environment* and *tags* parameters represent the environment being
665+
selected for. If unspecified, ``packaging.markers.default_environment()``
666+
and ``packaging.tags.sys_tags()`` are used.
667+
668+
The *extras* parameter represents the extras to install.
669+
670+
The *dependency_groups* parameter represents the groups to install. If
671+
unspecified, the default groups are used.
672+
"""
673+
if environment is None:
674+
environment = default_environment()
675+
if tags is None:
676+
tags = list(sys_tags())
677+
678+
# Validating the lock object covers some parts of the spec, such as checking
679+
# the lock file version, and conflicting sources for packages.
680+
# XXX we could document that we expect a valid lock object here.
681+
self.validate()
682+
683+
# #. Gather the extras and dependency groups to install and set ``extras`` and
684+
# ``dependency_groups`` for marker evaluation, respectively.
685+
#
686+
# #. ``extras`` SHOULD be set to the empty set by default.
687+
# #. ``dependency_groups`` SHOULD be the set created from
688+
# :ref:`pylock-default-groups` by default.
689+
env: dict[str, str | frozenset[str]] = {
690+
**cast("dict[str, str]", environment),
691+
"extras": frozenset(extras or []),
692+
"dependency_groups": frozenset(
693+
dependency_groups or self.default_groups or []
694+
),
695+
}
696+
env_python_version = environment.get("python_version")
697+
698+
# #. Check if the metadata version specified by :ref:`pylock-lock-version` is
699+
# supported; an error or warning MUST be raised as appropriate.
700+
# Covered by lock.validate() above.
641701

642-
class PylockSelectError(Exception):
643-
"""Base exception for errors raised by :func:`select()`."""
644-
645-
646-
def select(
647-
lock: Pylock,
648-
*,
649-
environment: Environment | None = None,
650-
tags: Sequence[Tag] | None = None,
651-
extras: Collection[str] | None = None,
652-
dependency_groups: Collection[str] | None = None,
653-
) -> Iterator[ # XXX or Iterable?
654-
tuple[
655-
Package,
656-
PackageVcs | PackageDirectory | PackageArchive | PackageWheel | PackageSdist,
657-
]
658-
]:
659-
"""Select what to install from the lock file.
660-
661-
The *environment* and *tags* parameters represent the environment being
662-
selected for. If unspecified, ``packaging.markers.default_environment()``
663-
and ``packaging.tags.sys_tags()`` are used.
664-
665-
The *extras* parameter represents the extras to install.
666-
667-
The *dependency_groups* parameter represents the groups to install. If
668-
unspecified, the default groups are used.
669-
"""
670-
if environment is None:
671-
environment = default_environment()
672-
if tags is None:
673-
tags = list(sys_tags())
674-
675-
# Validating the lock object covers some parts of the spec, such as checking
676-
# the lock file version, and conflicting sources for packages.
677-
# XXX we could document that we expect a valid lock object here.
678-
lock.validate()
679-
680-
# #. Gather the extras and dependency groups to install and set ``extras`` and
681-
# ``dependency_groups`` for marker evaluation, respectively.
682-
#
683-
# #. ``extras`` SHOULD be set to the empty set by default.
684-
# #. ``dependency_groups`` SHOULD be the set created from
685-
# :ref:`pylock-default-groups` by default.
686-
env: dict[str, str | frozenset[str]] = {
687-
**cast("dict[str, str]", environment),
688-
"extras": frozenset(extras or []), # XXX is normalization needed?
689-
"dependency_groups": frozenset(
690-
dependency_groups or lock.default_groups or []
691-
), # XXX is normalization needed?
692-
}
693-
env_python_version = environment.get("python_version")
694-
695-
# #. Check if the metadata version specified by :ref:`pylock-lock-version` is
696-
# supported; an error or warning MUST be raised as appropriate.
697-
# Covered by lock.validate() above.
698-
699-
# #. If :ref:`pylock-requires-python` is specified, check that the environment
700-
# being installed for meets the requirement; an error MUST be raised if it is
701-
# not met.
702-
if lock.requires_python is not None:
703-
if not env_python_version:
704-
raise PylockSelectError(
705-
f"Provided environment does not specify a Python version, "
706-
f"but the lock file requires Python {lock.requires_python!r}"
707-
)
708-
if not lock.requires_python.contains(env_python_version, prereleases=True):
709-
# XXX confirm prereleases=True
710-
raise PylockSelectError(
711-
f"Provided environment does not satisfy the Python version "
712-
f"requirement {lock.requires_python!r}"
713-
)
714-
715-
# #. If :ref:`pylock-environments` is specified, check that at least one of the
716-
# environment marker expressions is satisfied; an error MUST be raised if no
717-
# expression is satisfied.
718-
if lock.environments:
719-
for env_marker in lock.environments:
720-
if env_marker.evaluate(env, context="lock_file"): # XXX check context
721-
break
722-
else:
723-
raise PylockSelectError(
724-
"Provided environment does not satisfy any of the "
725-
"environments specified in the lock file"
726-
)
727-
728-
# #. For each package listed in :ref:`pylock-packages`:
729-
selected_packages_by_name: dict[str, tuple[int, Package]] = {}
730-
for package_index, package in enumerate(lock.packages):
731-
# #. If :ref:`pylock-packages-marker` is specified, check if it is satisfied;
732-
# if it isn't, skip to the next package.
733-
if package.marker and not package.marker.evaluate(
734-
env, context="requirement"
735-
): # XXX check context
736-
continue
737-
738-
# #. If :ref:`pylock-packages-requires-python` is specified, check if it is
739-
# satisfied; an error MUST be raised if it isn't.
740-
if package.requires_python:
702+
# #. If :ref:`pylock-requires-python` is specified, check that the environment
703+
# being installed for meets the requirement; an error MUST be raised if it is
704+
# not met.
705+
if self.requires_python is not None:
741706
if not env_python_version:
742707
raise PylockSelectError(
743708
f"Provided environment does not specify a Python version, "
744-
f"but package {package.name!r} at packages[{package_index}] "
745-
f"requires Python {package.requires_python!r}"
709+
f"but the lock file requires Python {self.requires_python!r}"
746710
)
747-
if not package.requires_python.contains(
748-
env_python_version, prereleases=True
749-
):
711+
if not self.requires_python.contains(env_python_version, prereleases=True):
750712
# XXX confirm prereleases=True
751713
raise PylockSelectError(
752714
f"Provided environment does not satisfy the Python version "
753-
f"requirement {package.requires_python!r} for package "
754-
f"{package.name!r} at packages[{package_index}]"
715+
f"requirement {self.requires_python!r}"
755716
)
756717

757-
# #. Check that no other conflicting instance of the package has been slated to
758-
# be installed; an error about the ambiguity MUST be raised otherwise.
759-
if package.name in selected_packages_by_name:
760-
raise PylockSelectError(
761-
f"Multiple packages with the name {package.name!r} are "
762-
f"selected at packages[{package_index}] and "
763-
f"packages[{selected_packages_by_name[package.name][0]}]"
764-
)
765-
766-
# #. Check that the source of the package is specified appropriately (i.e.
767-
# there are no conflicting sources in the package entry);
768-
# an error MUST be raised if any issues are found.
769-
# Covered by lock.validate() above.
770-
771-
# #. Add the package to the set of packages to install.
772-
selected_packages_by_name[package.name] = (package_index, package)
773-
774-
# #. For each package to be installed:
775-
for package_index, package in selected_packages_by_name.values():
776-
# - If :ref:`pylock-packages-vcs` is set:
777-
if package.vcs is not None:
778-
yield package, package.vcs
779-
780-
# - Else if :ref:`pylock-packages-directory` is set:
781-
elif package.directory is not None:
782-
yield package, package.directory
783-
784-
# - Else if :ref:`pylock-packages-archive` is set:
785-
elif package.archive is not None:
786-
yield package, package.archive
787-
788-
# - Else if there are entries for :ref:`pylock-packages-wheels`:
789-
elif package.wheels:
790-
# #. Look for the appropriate wheel file based on
791-
# :ref:`pylock-packages-wheels-name`; if one is not found then move on
792-
# to :ref:`pylock-packages-sdist` or an error MUST be raised about a
793-
# lack of source for the project.
794-
for package_wheel in package.wheels:
795-
try:
796-
assert package_wheel.name # XXX get name from path or url
797-
package_wheel_tags = parse_wheel_filename(package_wheel.name)[-1]
798-
except Exception as e:
799-
raise PylockSelectError(
800-
f"Invalid wheel filename {package_wheel.name!r} for "
801-
f"package {package.name!r} at packages[{package_index}]"
802-
) from e
803-
if not package_wheel_tags.isdisjoint(tags):
804-
yield package, package_wheel
718+
# #. If :ref:`pylock-environments` is specified, check that at least one of the
719+
# environment marker expressions is satisfied; an error MUST be raised if no
720+
# expression is satisfied.
721+
if self.environments:
722+
for env_marker in self.environments:
723+
if env_marker.evaluate(env, context="lock_file"): # XXX check context
805724
break
806725
else:
807-
if package.sdist is not None:
808-
yield package, package.sdist
809-
else:
726+
raise PylockSelectError(
727+
"Provided environment does not satisfy any of the "
728+
"environments specified in the lock file"
729+
)
730+
731+
# #. For each package listed in :ref:`pylock-packages`:
732+
selected_packages_by_name: dict[str, tuple[int, Package]] = {}
733+
for package_index, package in enumerate(self.packages):
734+
# #. If :ref:`pylock-packages-marker` is specified, check if it is
735+
# satisfied;if it isn't, skip to the next package.
736+
if package.marker and not package.marker.evaluate(
737+
env, context="requirement"
738+
): # XXX check context
739+
continue
740+
741+
# #. If :ref:`pylock-packages-requires-python` is specified, check if it is
742+
# satisfied; an error MUST be raised if it isn't.
743+
if package.requires_python:
744+
if not env_python_version:
810745
raise PylockSelectError(
811-
f"No matching wheel found matching the provided tags "
812-
f"for package {package.name!r} at packages[{package_index}], "
813-
f"and no sdist available as a fallback"
746+
f"Provided environment does not specify a Python version, "
747+
f"but package {package.name!r} at packages[{package_index}] "
748+
f"requires Python {package.requires_python!r}"
749+
)
750+
if not package.requires_python.contains(
751+
env_python_version, prereleases=True
752+
):
753+
# XXX confirm prereleases=True
754+
raise PylockSelectError(
755+
f"Provided environment does not satisfy the Python version "
756+
f"requirement {package.requires_python!r} for package "
757+
f"{package.name!r} at packages[{package_index}]"
814758
)
815759

816-
# - Else if no :ref:`pylock-packages-wheels` file is found or
817-
# :ref:`pylock-packages-sdist` is solely set:
818-
elif package.sdist is not None:
819-
yield package, package.sdist
760+
# #. Check that no other conflicting instance of the package has been slated
761+
# to be installed; an error about the ambiguity MUST be raised otherwise.
762+
if package.name in selected_packages_by_name:
763+
raise PylockSelectError(
764+
f"Multiple packages with the name {package.name!r} are "
765+
f"selected at packages[{package_index}] and "
766+
f"packages[{selected_packages_by_name[package.name][0]}]"
767+
)
820768

821-
else:
769+
# #. Check that the source of the package is specified appropriately (i.e.
770+
# there are no conflicting sources in the package entry);
771+
# an error MUST be raised if any issues are found.
822772
# Covered by lock.validate() above.
823-
raise NotImplementedError
773+
774+
# #. Add the package to the set of packages to install.
775+
selected_packages_by_name[package.name] = (package_index, package)
776+
777+
# #. For each package to be installed:
778+
for package_index, package in selected_packages_by_name.values():
779+
# - If :ref:`pylock-packages-vcs` is set:
780+
if package.vcs is not None:
781+
yield package, package.vcs
782+
783+
# - Else if :ref:`pylock-packages-directory` is set:
784+
elif package.directory is not None:
785+
yield package, package.directory
786+
787+
# - Else if :ref:`pylock-packages-archive` is set:
788+
elif package.archive is not None:
789+
yield package, package.archive
790+
791+
# - Else if there are entries for :ref:`pylock-packages-wheels`:
792+
elif package.wheels:
793+
# #. Look for the appropriate wheel file based on
794+
# :ref:`pylock-packages-wheels-name`; if one is not found then move
795+
# on to :ref:`pylock-packages-sdist` or an error MUST be raised about
796+
# a lack of source for the project.
797+
for package_wheel in package.wheels:
798+
try:
799+
assert package_wheel.name # XXX get name from path or url
800+
package_wheel_tags = parse_wheel_filename(package_wheel.name)[
801+
-1
802+
]
803+
except Exception as e:
804+
raise PylockSelectError(
805+
f"Invalid wheel filename {package_wheel.name!r} for "
806+
f"package {package.name!r} at packages[{package_index}]"
807+
) from e
808+
if not package_wheel_tags.isdisjoint(tags):
809+
yield package, package_wheel
810+
break
811+
else:
812+
if package.sdist is not None:
813+
yield package, package.sdist
814+
else:
815+
raise PylockSelectError(
816+
f"No wheel found matching the provided tags "
817+
f"for package {package.name!r} "
818+
f"at packages[{package_index}], "
819+
f"and no sdist available as a fallback"
820+
)
821+
822+
# - Else if no :ref:`pylock-packages-wheels` file is found or
823+
# :ref:`pylock-packages-sdist` is solely set:
824+
elif package.sdist is not None:
825+
yield package, package.sdist
826+
827+
else:
828+
# Covered by lock.validate() above.
829+
raise NotImplementedError

0 commit comments

Comments
 (0)