Repository: omni-us/jsonargparse
Pull request #403 (2023-10-18 05:32:23)

AST resolver now supports some local import cases
## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [x] Did you update **the documentation**? (readme and public docstrings)
- [x] Did you write **unit tests** such that there is 100% coverage on related code? (required for bug fixes and new features)
- [x] Did you verify that new and existing **tests pass locally**?
- [x] Did you make sure that all changes preserve **backward compatibility**?
- [x] Did you update **the CHANGELOG**? (not for typos, docs, test updates, or minor internal changes/refactors)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=403)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=403&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=403) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=403&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=403&metric=new_duplicated_lines_density&view=list)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
All modified lines are covered by tests :white_check_mark:
> Comparison is base [(`bbeb9d1`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/bbeb9d1dd8921dbb53052482e1c4a3ab45c12ebb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00% compared to head [(`4e23ac2`)](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #403   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         5847      5879   +32     
=========================================
+ Hits          5847      5879   +32     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [py3.10](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.16% <100.00%> (+0.06%)` | :arrow_up: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.70% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.29% <12.76%> (-0.20%)` | :arrow_down: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.01% <12.76%> (-0.20%)` | :arrow_down: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.70% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.14% <100.00%> (+0.06%)` | :arrow_up: |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.70% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.70% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.74% <100.00%> (+0.06%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.48% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.46% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.89% <100.00%> (+0.06%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.50% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.48% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [jsonargparse/\_core.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19jb3JlLnB5) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/\_parameter\_resolvers.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19wYXJhbWV0ZXJfcmVzb2x2ZXJzLnB5) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/403?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 28ec710..2bfec93 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -20,6 +20,7 @@ Added
 - Support for on parse argument links with target subclasses in a list (`#394
   <https://github.com/omni-us/jsonargparse/issues/394>`__, `lightning#18161
   <https://github.com/Lightning-AI/lightning/issues/18161>`__).
+- AST resolver now supports some local import cases.
 
 Fixed
 ^^^^^
diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst
index 39ca493..e134736 100644
--- a/DOCUMENTATION.rst
+++ b/DOCUMENTATION.rst
@@ -1703,6 +1703,11 @@ unrelated to these variables.
         SomeClass.a_class_method(*args, **kwargs)
 
 
+    def calls_local_import(**kwargs):
+        import some_module
+        some_module.a_callable(**kwargs)
+
+
     def pops_from_kwargs(**kwargs):
         val = kwargs.pop("name", "default")
 
diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py
index d04bfc0..1857bc3 100644
--- a/jsonargparse/_core.py
+++ b/jsonargparse/_core.py
@@ -999,7 +999,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
         elif debug_mode_active():
             self._logger.debug("Debug enabled, thus raising exception instead of exit.")
             raise argument_error(message) from ex
-        self.print_usage()
+        self.print_usage(sys.stderr)
         sys.stderr.write(f"error: {message}\n")
         self.exit(2)
 
diff --git a/jsonargparse/_parameter_resolvers.py b/jsonargparse/_parameter_resolvers.py
index 7c7bef3..0addc68 100644
--- a/jsonargparse/_parameter_resolvers.py
+++ b/jsonargparse/_parameter_resolvers.py
@@ -506,11 +506,11 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
         do_generic_visit = True
         for key, value in self.find_values.items():
             if ast_is_assign_with_value(node, value):
-                self.values_found.append((key, node))
+                self.add_value(key, node)
                 do_generic_visit = False
                 break
             elif ast_is_dict_assign_with_value(node, value):
-                self.values_found.append((key, node))
+                self.add_value(key, node)
                 do_generic_visit = False
         if do_generic_visit:
             if ast_is_dict_assign(node):
@@ -531,11 +531,11 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
                 if isinstance(node.func, ast.Attribute):
                     value_dump = ast.dump(node.func.value)
                     if value_dump in self.dict_assigns:
-                        self.values_found.append((key, self.dict_assigns[value_dump]))
+                        self.add_value(key, self.dict_assigns[value_dump])
                         continue
-                self.values_found.append((key, node))
+                self.add_value(key, node)
             elif ast_is_kwargs_pop_or_get(node, value_dump):
-                self.values_found.append((key, node))
+                self.add_value(key, node)
         self.generic_visit(node)
 
     def visit_If(self, node):
@@ -550,14 +550,45 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
             node = ast.If(test=ast.Constant(value=True), body=body, orelse=[])
         self.generic_visit(node)
 
+    def visit_Import(self, node: Union[ast.Import, ast.ImportFrom]) -> None:
+        for alias in node.names:
+            self.import_names[alias.asname or alias.name] = node
+
+    def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
+        self.visit_Import(node)
+
+    def add_value(self, key, node):
+        source = None
+        if isinstance(node, ast.Call):
+            name = False
+            if isinstance(node.func, ast.Name):
+                name = node.func.id
+            elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
+                name = node.func.value.id
+            if name and name in self.import_names:
+                source = self.import_names[name]
+        self.values_found.append((key, node, source))
+
     def find_values_usage(self, values):
         self.find_values = values
         self.values_found = []
         self.dict_assigns = {}
+        self.import_names = {}
         self.visit(self.component_node)
         return self.values_found
 
-    def get_node_component(self, node) -> Optional[Tuple[Type, Optional[str]]]:
+    def get_component_from_source(self, name, source):
+        aliases = {}
+        ast_exec = ast.parse("")
+        ast_exec.body = [source]
+        try:
+            exec(compile(ast_exec, filename="<ast>", mode="exec"), aliases, aliases)
+        except Exception as ex:
+            if self.logger:
+                self.logger.debug(f"Failed to get '{name}' from '{ast_str(source)}'", exc_info=ex)
+        return aliases.get(name)
+
+    def get_node_component(self, node, source) -> Optional[Tuple[Type, Optional[str]]]:
         function_or_class = method_or_property = None
         module = inspect.getmodule(self.component)
         if isinstance(node.func, ast.Name):
@@ -565,12 +596,18 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
                 function_or_class = self.parent
             elif hasattr(module, node.func.id):
                 function_or_class = getattr(module, node.func.id)
+            elif source:
+                function_or_class = self.get_component_from_source(node.func.id, source)
         elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
             if self.parent and ast.dump(node.func.value) == ast.dump(ast_variable_load(self.self_name)):
                 function_or_class = self.parent
                 method_or_property = node.func.attr
-            elif hasattr(module, node.func.value.id):
-                container = getattr(module, node.func.value.id)
+            else:
+                container = None
+                if hasattr(module, node.func.value.id):
+                    container = getattr(module, node.func.value.id)
+                elif source:
+                    container = self.get_component_from_source(node.func.value.id, source)
                 if inspect.isclass(container):
                     function_or_class = container
                     method_or_property = node.func.attr
@@ -581,7 +618,7 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
             return None
         return function_or_class, method_or_property
 
-    def match_call_that_uses_attr(self, node, attr_name):
+    def match_call_that_uses_attr(self, node, source, attr_name):
         params = None
         if isinstance(node, ast.Call):
             params = []
@@ -591,7 +628,7 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
                 if kwarg.arg:
                     self.log_debug(f"kwargs attribute given as keyword parameter not supported: {ast_str(node)}")
                 else:
-                    get_param_args = self.get_node_component(node)
+                    get_param_args = self.get_node_component(node, source)
                     if get_param_args:
                         try:
                             params = get_signature_parameters(*get_param_args, logger=self.logger)
@@ -686,7 +723,7 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
         params_list = []
         kwargs_value = kwargs_name and values_to_find[kwargs_name]
         kwargs_value_dump = kwargs_value and ast.dump(kwargs_value)
-        for node in [v for k, v in values_found if k == kwargs_name]:
+        for node, source in [(v, s) for k, v, s in values_found if k == kwargs_name]:
             if isinstance(node, ast.Call):
                 if ast_is_kwargs_pop_or_get(node, kwargs_value_dump):
                     param = self.get_kwargs_pop_or_get_parameter(node, self.component, self.parent, self.doc_params)
@@ -704,7 +741,7 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
                             self.logger,
                         )
                 else:
-                    get_param_args = self.get_node_component(node)
+                    get_param_args = self.get_node_component(node, source)
                     if get_param_args:
                         params = get_signature_parameters(*get_param_args, logger=self.logger)
                 params = remove_given_parameters(node, params)
@@ -757,8 +794,8 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
         values_found = self.find_values_usage(values_to_find)
         matched = []
         if values_found:
-            for _, node in values_found:
-                match = self.match_call_that_uses_attr(node, attr_name)
+            for _, node, source in values_found:
+                match = self.match_call_that_uses_attr(node, source, attr_name)
                 if match:
                     self.add_node_origins(match, node)
                     matched.append(match)


Test patch:
diff --git a/jsonargparse_tests/test_parameter_resolvers.py b/jsonargparse_tests/test_parameter_resolvers.py
index 0f2035e..483749d 100644
--- a/jsonargparse_tests/test_parameter_resolvers.py
+++ b/jsonargparse_tests/test_parameter_resolvers.py
@@ -123,6 +123,56 @@ class ClassE2:
         return self.fn(**self._kwd)
 
 
+class AttributeLocalImport1:
+    def __init__(self, **kwargs):
+        self._kwd = dict(**kwargs)
+
+    def run(self):
+        from jsonargparse import set_loader
+
+        return set_loader(**self._kwd)
+
+
+class AttributeLocalImport2:
+    def __init__(self, **kwargs):
+        self._kwd = dict(**kwargs)
+
+    def run(self):
+        import jsonargparse as ja
+
+        return ja.set_loader(**self._kwd)
+
+
+class AttributeLocalImport3:
+    def __init__(self, **kwargs):
+        self._kwd = dict(**kwargs)
+
+    def run(self):
+        from jsonargparse import set_loader
+
+        return set_loader(**self._kwd)
+
+
+class AttributeLocalImport4:
+    def __init__(self, **kwargs):
+        self._kwd = dict(**kwargs)
+
+    def run(self):
+        from jsonargparse import set_loader as sl
+
+        return sl(**self._kwd)
+
+
+class AttributeLocalImportFailure:
+    def __init__(self, **kwargs):
+        self._kwd = dict(**kwargs)
+
+    def run(self):
+        from jsonargparse import does_not_exist
+
+        return does_not_exist(**self._kwd)
+
+
 class ClassF1:
     def __init__(self, **kw):
         self._ini = dict(k2=4)
@@ -421,6 +471,12 @@ def function_module_class(**kwds):
     return calendar.Calendar(**kwds)
 
 
+def function_local_import(**kwds):
+    from jsonargparse import set_loader
+
+    return set_loader(**kwds)
+
+
 constant_boolean_1 = True
 constant_boolean_2 = False
 
@@ -527,6 +583,24 @@ def test_get_params_class_with_kwargs_in_dict_attribute():
         assert_params(get_params(ClassF1), [])
 
 
+@pytest.mark.parametrize(
+    "cls",
+    [AttributeLocalImport1, AttributeLocalImport2, AttributeLocalImport3, AttributeLocalImport4],
+)
+def test_get_params_local_import_with_kwargs_in_dict_attribute(cls):
+    params = get_params(cls)
+    assert ["mode", "loader_fn", "exceptions"] == [p.name for p in params]
+    with source_unavailable():
+        assert get_params(cls) == []
+
+
+def test_get_params_local_import_failure_with_kwargs_in_dict_attribute(logger):
+    with capture_logs(logger) as logs:
+        params = get_params(AttributeLocalImportFailure, logger=logger)
+    assert params == []
+    assert "Failed to get 'does_not_exist'" in logs.getvalue()
+
+
 def test_get_params_class_kwargs_in_attr_method_conditioned_on_arg():
     params = get_params(ClassG)
     assert_params(
@@ -706,6 +780,11 @@ def test_get_params_function_module_class():
     assert ["firstweekday"] == [p.name for p in params]
 
 
+def test_get_params_function_local_import():
+    params = get_params(function_local_import)
+    assert ["mode", "loader_fn", "exceptions"] == [p.name for p in params]
+
+
 def test_get_params_function_constant_boolean():
     assert_params(get_params(function_constant_boolean), ["k1", "pk1", "k2"])
     with patch.dict(function_constant_boolean.__globals__, {"constant_boolean_1": False}):