Skip to content

Commit

Permalink
Allow overriding forced_requirements when resolving a specific config
Browse files Browse the repository at this point in the history
This allows you to override the requirements per-config instead of having
to modify the global setting stored on the Resolver instance.
  • Loading branch information
MHendricks committed Oct 8, 2024
1 parent b55116b commit 7d9f656
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 11 deletions.
34 changes: 28 additions & 6 deletions hab/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ class Resolver(object):
prereleases (bool, optional): When resolving distro versions, should
pre-releases be included in the latest version. If not specified uses the
value specified in site for the ``"prereleases"`` value.
forced_requirements (list, optional): A list of additional version requirements
to respect even if they are not specified in a config. This is provided for
ease of hab package development and should not be used in production.
forced_requirements (list, optional): A list of additional distro version
requirements to respect even if they are not specified in a config.
This is how `optional_distros` are enabled.
Has :py:meth:`hab.solvers.Solver.simplify_requirements` called on it.
"""

_instances = {}
Expand Down Expand Up @@ -328,11 +329,32 @@ def parse_distros(self, distro_paths, forest=None):
logger.debug(str(error))
return forest

def resolve(self, uri):
"""Find the closest configuration and reduce it into its final form."""
def resolve(self, uri, forced_requirements=None):
"""Find the closest configuration and reduce it into its final form.
Args:
uri (str): The URI to resolve.
forced_requirements (list, optional): A list of additional distro version
requirements to respect even if they are not specified in a config.
Has :py:meth:`hab.solvers.Solver.simplify_requirements` called on it.
"""
uri = self.uri_validate(uri)
context = self.closest_config(uri)
return context.reduced(self, uri=uri)
try:
# Apply the custom forced_requirements if provided
if forced_requirements is not None:
current = self.forced_requirements

self.forced_requirements = Solver.simplify_requirements(
forced_requirements
)
logger.warning(f"Forced Requirements overridden: {forced_requirements}")

return context.reduced(self, uri=uri)
finally:
# Ensure the forced_requirements are restored no matter what.
if forced_requirements is not None:
self.forced_requirements = current

def resolve_requirements(self, requirements):
"""Recursively solve the provided requirements into a final list of requirements.
Expand Down
92 changes: 87 additions & 5 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,9 @@ def test_reduced(resolver, helpers):


class TestResolveRequirements:
def cfg_versions_to_dict(self, cfg):
return {v.distro_name: Requirement(v.name) for v in cfg.versions}

def test_simple(self, resolver):
requirements = {
"the_dcc": Requirement("the_dcc"),
Expand Down Expand Up @@ -568,18 +571,97 @@ def test_forced_requirements_uri(self, resolver, helpers):
# We are checking cfg.versions, so these need to be resolved to `==` requirements
check = ["aliased==2.0", "houdini19.5==19.5.493"]

def cfg_versions_to_dict(cfg):
return {v.distro_name: Requirement(v.name) for v in cfg.versions}

# Forced requirement includes direct distro assignments from the config
cfg = resolver_forced.resolve("app/aliased")
versions = cfg_versions_to_dict(cfg)
versions = self.cfg_versions_to_dict(cfg)
helpers.assert_requirements_equal(versions, check)

# Check that forced requirements are correctly applied even if a config
# inherits it's distros from a parent config.
cfg = resolver_forced.resolve("app/aliased/config")
versions = cfg_versions_to_dict(cfg)
versions = self.cfg_versions_to_dict(cfg)
helpers.assert_requirements_equal(versions, check)

def test_override_forced(self, uncached_resolver, helpers):
"""Test forced_requirements passed to `resolve` with global
forced_requirements temporarily replaces any already set on the Resolver.
"""
hou_requirement = "houdini19.5==19.5.493"
uncached_resolver = Resolver(
site=uncached_resolver.site, forced_requirements=[hou_requirement]
)
global_requirements = [hou_requirement]

# This resolver has the expected forced requirements
helpers.assert_requirements_equal(
uncached_resolver.forced_requirements, global_requirements
)

# Passing an override forced_requirement is respected
cfg = uncached_resolver.resolve("app/aliased", forced_requirements=["maya2024"])
# and the original forced requirements are restored
helpers.assert_requirements_equal(
uncached_resolver.forced_requirements, global_requirements
)

# Check that the overridden requirements were actually respected
# The houdini requirement was removed and maya inserted
check = ["aliased==2.0", "maya2024==2024.0"]
versions = self.cfg_versions_to_dict(cfg)
helpers.assert_requirements_equal(versions, check)

def test_override_forced_empty(self, uncached_resolver, helpers):
"""Test forced_requirements passed to `resolve` with global
forced_requirements temporarily replaces any already set on the Resolver.
"""
hou_requirement = "houdini19.5==19.5.493"
uncached_resolver = Resolver(
site=uncached_resolver.site, forced_requirements=[hou_requirement]
)
global_requirements = [hou_requirement]
# This resolver has the expected forced requirements
helpers.assert_requirements_equal(
uncached_resolver.forced_requirements, global_requirements
)

# Passing an forced_requirement override removes the global requirement
cfg = uncached_resolver.resolve("app/aliased", forced_requirements=[])
# and the original forced requirements are restored
helpers.assert_requirements_equal(
uncached_resolver.forced_requirements, global_requirements
)

# Check that the overridden requirements were actually respected
# The houdini requirement was removed and maya inserted
check = ["aliased==2.0"]
versions = self.cfg_versions_to_dict(cfg)
helpers.assert_requirements_equal(versions, check)

def test_override_forced_unset(self, uncached_resolver, helpers):
"""Test forced_requirements override passed to `resolve` without any
global forced_requirements set on the Resolver."""

# This resolver has no existing forced_requirements
assert uncached_resolver.forced_requirements == {}

# Ensure our config doesn't include houdini19.5 by default
cfg = uncached_resolver.resolve("app/aliased")
# The previous call didn't modify `forced_requirements`
assert uncached_resolver.forced_requirements == {}
check = ["aliased==2.0"]
versions = self.cfg_versions_to_dict(cfg)
helpers.assert_requirements_equal(versions, check)

# Passing an override forced_requirement is respected
cfg = uncached_resolver.resolve(
"app/aliased", forced_requirements=["houdini19.5==19.5.493"]
)
# and the original forced requirements are restored
assert uncached_resolver.forced_requirements == {}

# Check that the overridden requirements were actually respected
check = ["aliased==2.0", "houdini19.5==19.5.493"]
versions = self.cfg_versions_to_dict(cfg)
helpers.assert_requirements_equal(versions, check)


Expand Down

0 comments on commit 7d9f656

Please sign in to comment.