Example #1
0
 def group(value_component_type, process_val_func) -> list[RankedValue]:
     # We group any values that are merged together, so that the history can reflect
     # merges vs. replacements in a useful way. E.g., if we merge [a, b] and [c],
     # and then replace it with [d, e], the history will contain:
     #   - [d, e] (from command-line flag)
     #   - [a, b, c] (from env var, from config)
     # And similarly for dicts.
     grouped: list[list[RankedValue]] = [[]]
     for ranked_val in ranked_vals:
         if ranked_val.value and ranked_val.value.action == value_component_type.REPLACE:
             grouped.append([])
         grouped[-1].append(ranked_val)
     return [
         RankedValue(
             grp[-1].rank,
             process_val_func(
                 value_component_type.merge(
                     rv.value for rv in grp if rv.value is not None
                 ).val
             ),
             ", ".join(rv.details for rv in grp if rv.details),
         )
         for grp in grouped
         if grp
     ]
Example #2
0
    def execute(self):
        requested_compiler = JvmPlatform.global_instance().get_options(
        ).compiler
        if requested_compiler != self.compiler_name:
            return

        if requested_compiler == self.Compiler.ZINC and self.compiler_name == self.Compiler.RSC:
            # Issue a deprecation warning (above) and rewrite zinc to rsc, as zinc is being deprecated.
            JvmPlatform.global_instance().get_options().compiler = RankedValue(
                0, self.compiler_name)
        elif requested_compiler != self.compiler_name:
            # If the requested compiler is not the one supported by this task, log and abort
            self.context.log.debug(
                'Requested an unsupported compiler [{}], aborting'.format(
                    requested_compiler))
            return

        # In case we have no relevant targets and return early, create the requested product maps.
        self.create_empty_extra_products()

        relevant_targets = list(self.context.targets(predicate=self.select))

        if not relevant_targets:
            return

        # Clone the compile_classpath to the runtime_classpath.
        classpath_product = self.create_runtime_classpath()

        fingerprint_strategy = DependencyContext.global_instance(
        ).create_fingerprint_strategy(classpath_product)
        # Note, JVM targets are validated (`vts.update()`) as they succeed.  As a result,
        # we begin writing artifacts out to the cache immediately instead of waiting for
        # all targets to finish.
        with self.invalidated(relevant_targets,
                              invalidate_dependents=True,
                              fingerprint_strategy=fingerprint_strategy,
                              topological_order=True) as invalidation_check:

            compile_contexts = {
                vt.target: self.create_compile_context(vt.target,
                                                       vt.results_dir)
                for vt in invalidation_check.all_vts
            }

            self.do_compile(
                invalidation_check,
                compile_contexts,
                classpath_product,
            )

            if not self.get_options().use_classpath_jars:
                # Once compilation has completed, replace the classpath entry for each target with
                # its jar'd representation.
                for ccs in compile_contexts.values():
                    cc = self.select_runtime_context(ccs)
                    for conf in self._confs:
                        classpath_product.remove_for_target(
                            cc.target, [(conf, cc.classes_dir)])
                        classpath_product.add_for_target(
                            cc.target, [(conf, cc.jar_file)])
Example #3
0
    def test_rsc_dep_for_scala_java_and_test_targets(self):
        self.set_options(workflow=RankedValue(
            value=RscCompile.JvmCompileWorkflowType.rsc_and_zinc,
            rank=RankedValue.CONFIG,
        ))
        self.init_dependencies_for_scala_libraries()

        scala_dep = self.make_target('scala/classpath:scala_dep',
                                     target_type=ScalaLibrary,
                                     sources=['com/example/Bar.scala'])
        java_target = self.make_target('java/classpath:java_lib',
                                       target_type=JavaLibrary,
                                       sources=['com/example/Foo.java'],
                                       dependencies=[scala_dep],
                                       tags={'use-compiler:zinc-only'})
        scala_target = self.make_target('scala/classpath:scala_lib',
                                        target_type=ScalaLibrary,
                                        sources=['com/example/Foo.scala'],
                                        dependencies=[scala_dep])

        test_target = self.make_target('scala/classpath:scala_test',
                                       target_type=JUnitTests,
                                       sources=['com/example/Test.scala'],
                                       dependencies=[scala_target],
                                       tags={'use-compiler:zinc-only'})

        with temporary_dir() as tmp_dir:
            invalid_targets = [
                java_target, scala_target, scala_dep, test_target
            ]
            task = self.create_task_with_target_roots(
                target_roots=[java_target, scala_target, test_target])

            jobs = task._create_compile_jobs(
                compile_contexts=self.create_compile_contexts(
                    invalid_targets, task, tmp_dir),
                invalid_targets=invalid_targets,
                invalid_vts=self.wrap_in_vts(invalid_targets),
                classpath_product=None)

            dependee_graph = self.construct_dependee_graph_str(jobs, task)

            self.assertEqual(
                dedent("""
                     zinc[zinc-java](java/classpath:java_lib) <- {}
                     rsc(scala/classpath:scala_lib) <- {
                       zinc[zinc-only](scala/classpath:scala_test)
                     }
                     zinc[rsc-and-zinc](scala/classpath:scala_lib) <- {}
                     rsc(scala/classpath:scala_dep) <- {
                       rsc(scala/classpath:scala_lib),
                       zinc[rsc-and-zinc](scala/classpath:scala_lib),
                       zinc[zinc-only](scala/classpath:scala_test)
                     }
                     zinc[rsc-and-zinc](scala/classpath:scala_dep) <- {
                       zinc[zinc-java](java/classpath:java_lib)
                     }
                     zinc[zinc-only](scala/classpath:scala_test) <- {}""").
                strip(), dependee_graph)
    def test_unknown_values(self) -> None:
        ob = OptionValueContainerBuilder()
        ob.foo = RankedValue(Rank.HARDCODED, 1)
        o = ob.build()
        self.assertEqual(1, o.foo)

        with self.assertRaises(AttributeError):
            o.bar
Example #5
0
    def test_deepcopy(self) -> None:
        # copy semantics can get hairy when overriding __setattr__/__getattr__, so we test them.
        o = OptionValueContainer()
        o.foo = RankedValue(RankedValue.FLAG, 1)
        o.bar = RankedValue(RankedValue.FLAG, {"a": 111})

        p = copy.deepcopy(o)

        # Verify that the result is in fact a copy.
        self.assertEqual(1, p.foo)  # Has original attribute.
        o.baz = RankedValue(RankedValue.FLAG, 42)
        self.assertFalse(hasattr(p, "baz"))  # Does not have attribute added after the copy.

        # Verify that it's a deep copy by modifying a referent in o and reading it in p.
        # TODO: add type hints to ranked_value.py and option_value_container.py so that this works.
        o.bar["b"] = 222  # type: ignore[index]
        self.assertEqual({"a": 111}, p.bar)
Example #6
0
    def test_deepcopy(self):
        # copy semantics can get hairy when overriding __setattr__/__getattr__, so we test them.
        o = OptionValueContainer()
        o.foo = RankedValue(RankedValue.FLAG, 1)
        o.bar = RankedValue(RankedValue.FLAG, {'a': 111})

        p = copy.deepcopy(o)

        # Verify that the result is in fact a copy.
        self.assertEqual(1, p.foo)  # Has original attribute.
        o.baz = RankedValue(RankedValue.FLAG, 42)
        self.assertFalse(hasattr(
            p, 'baz'))  # Does not have attribute added after the copy.

        # Verify that it's a deep copy by modifying a referent in o and reading it in p.
        o.bar['b'] = 222
        self.assertEqual({'a': 111}, p.bar)
Example #7
0
    def test_unknown_values(self) -> None:
        ob = OptionValueContainerBuilder()
        ob.foo = RankedValue(Rank.HARDCODED, 1)
        o = ob.build()
        assert 1 == o.foo

        with pytest.raises(AttributeError):
            o.bar
Example #8
0
    def test_copy(self) -> None:
        # copy semantics can get hairy when overriding __setattr__/__getattr__, so we test them.
        ob = OptionValueContainerBuilder()
        ob.foo = RankedValue(Rank.FLAG, 1)
        ob.bar = RankedValue(Rank.FLAG, {"a": 111})

        p = ob.build()
        z = ob.build()

        # Verify that the result is in fact a copy.
        assert 1 == p.foo  # Has original attribute.
        ob.baz = RankedValue(Rank.FLAG, 42)
        assert not hasattr(
            p, "baz")  # Does not have attribute added after the copy.

        # Verify that it's a shallow copy by modifying a referent in o and reading it in p.
        p.bar["b"] = 222
        assert {"a": 111, "b": 222} == z.bar
Example #9
0
 def test_value_ranking(self) -> None:
     ob = OptionValueContainerBuilder()
     ob.foo = RankedValue(Rank.CONFIG, 11)
     o = ob.build()
     assert 11 == o.foo
     assert Rank.CONFIG == o.get_rank("foo")
     ob.foo = RankedValue(Rank.HARDCODED, 22)
     o = ob.build()
     assert 11 == o.foo
     assert Rank.CONFIG == o.get_rank("foo")
     ob.foo = RankedValue(Rank.ENVIRONMENT, 33)
     o = ob.build()
     assert 33 == o.foo
     assert Rank.ENVIRONMENT == o.get_rank("foo")
     ob.foo = RankedValue(Rank.FLAG, 44)
     o = ob.build()
     assert 44 == o.foo
     assert Rank.FLAG == o.get_rank("foo")
Example #10
0
    def setUp(self):
        super().setUp()

        # We're tied tightly to pex implementation details here faking out a python binary that outputs
        # only one value no matter what arguments, environment or input stream it has attached. That
        # value is the interpreter identity which is a JSON dict with exactly the following keys:
        # binary, python_tag, abi_tag, platform_tag, version, supported_tags, env_markers.
        def fake_interpreter(python_tag: str, abi_tag: str, version: Tuple[int, int, int]):
            interpreter_dir = safe_mkdtemp()
            binary = os.path.join(interpreter_dir, "python")
            values = dict(
                binary=binary,
                python_tag=python_tag,
                abi_tag=abi_tag,
                platform_tag="",
                version=version,
                supported_tags=[],
                env_markers={},
            )
            id_str = json.dumps(values)
            with open(binary, "w") as fp:
                fp.write(
                    dedent(
                        f"""
                        #!{PythonInterpreter.get().binary}
                        from __future__ import print_function

                        print({id_str!r})
                        """
                    ).strip()
                )
            chmod_plus_x(binary)
            return PythonInterpreter.from_binary(binary)

        # impl, abi, impl_version, major, minor, patch
        self.fake_interpreters = [
            fake_interpreter(python_tag="ip", abi_tag="ip2", version=(2, 77, 777)),
            fake_interpreter(python_tag="ip", abi_tag="ip2", version=(2, 88, 888)),
            fake_interpreter(python_tag="ip", abi_tag="ip2", version=(2, 99, 999)),
        ]

        self.set_options_for_scope(
            PythonSetup.options_scope,
            interpreter_constraints=RankedValue(RankedValue.CONFIG, ["IronPython>=2.55"]),
            interpreter_search_paths=[interpreter.binary for interpreter in self.fake_interpreters],
        )

        self.reqtgt = self.make_target(
            spec="req", target_type=PythonRequirementLibrary, requirements=[],
        )
        self.tgt1 = self._fake_target("tgt1")
        self.tgt2 = self._fake_target("tgt2", compatibility=["IronPython>2.77.777"])
        self.tgt3 = self._fake_target("tgt3", compatibility=["IronPython>2.88.888"])
        self.tgt4 = self._fake_target("tgt4", compatibility=["IronPython<2.99.999"])
        self.tgt20 = self._fake_target("tgt20", dependencies=[self.tgt2])
        self.tgt30 = self._fake_target("tgt30", dependencies=[self.tgt3])
        self.tgt40 = self._fake_target("tgt40", dependencies=[self.tgt4])
 def test_value_ranking(self) -> None:
     ob = OptionValueContainerBuilder()
     ob.foo = RankedValue(Rank.CONFIG, 11)
     o = ob.build()
     self.assertEqual(11, o.foo)
     self.assertEqual(Rank.CONFIG, o.get_rank("foo"))
     ob.foo = RankedValue(Rank.HARDCODED, 22)
     o = ob.build()
     self.assertEqual(11, o.foo)
     self.assertEqual(Rank.CONFIG, o.get_rank("foo"))
     ob.foo = RankedValue(Rank.ENVIRONMENT, 33)
     o = ob.build()
     self.assertEqual(33, o.foo)
     self.assertEqual(Rank.ENVIRONMENT, o.get_rank("foo"))
     ob.foo = RankedValue(Rank.FLAG, 44)
     o = ob.build()
     self.assertEqual(44, o.foo)
     self.assertEqual(Rank.FLAG, o.get_rank("foo"))
Example #12
0
def create_option_value_container(
    default_rank: Rank = Rank.NONE, **options: RankedValue | Value
) -> OptionValueContainer:
    scoped_options = OptionValueContainerBuilder()
    for key, value in options.items():
        if not isinstance(value, RankedValue):
            value = RankedValue(default_rank, value)
        setattr(scoped_options, key, value)
    return scoped_options.build()
Example #13
0
def _create_scoped_options(
        default_rank: Rank, **options: Union[RankedValue,
                                             Value]) -> OptionValueContainer:
    scoped_options = OptionValueContainer()
    for key, value in options.items():
        if not isinstance(value, RankedValue):
            value = RankedValue(default_rank, value)
        setattr(scoped_options, key, value)
    return scoped_options
Example #14
0
 def _format_record(self, record):
   value_color = green if self.get_options().colors else lambda x: x
   rank_color = self._rank_color(record.rank)
   return '{value} {rank}'.format(
     value=value_color(str(record.value)),
     rank=rank_color('(from {rank}{details})'.format(
       rank=RankedValue.get_rank_name(record.rank),
       details=' {}'.format(record.details) if record.details else '',
     )),
   )
Example #15
0
 def _format_record(self, record):
   value_color = green if self.get_options().colors else lambda x: x
   rank_color = self._rank_color(record.rank)
   return '{value} {rank}'.format(
     value=value_color(str(record.value)),
     rank=rank_color('(from {rank}{details})'.format(
       rank=RankedValue.get_rank_name(record.rank),
       details=' {}'.format(record.details) if record.details else '',
     )),
   )
Example #16
0
 def register_options(cls, register):
   super(ExplainOptionsTask, cls).register_options(register)
   register('--scope', help='Only show options in this scope.')
   register('--name', help='Only show options with this name.')
   register('--rank', choices=RankedValue.get_names(),
            help='Only show options with at least this importance.')
   register('--show-history', action='store_true', default=False,
            help='Show the previous values options had before being overridden.')
   register('--only-overridden', action='store_true', default=False,
            help='Only show values that overrode defaults.')
Example #17
0
 def register(*args, **kwargs):
   default = kwargs.get('default')
   if default is None:
     if kwargs.get('type') == bool:
       default = False
     if kwargs.get('type') == list:
       default = []
   for flag_name in args:
     normalized_flag_name = flag_name.lstrip('-').replace('-', '_')
     defaults[normalized_flag_name] = RankedValue(RankedValue.HARDCODED, default)
Example #18
0
    def test_resolve_conflicting_options(self) -> None:
        resolve_options = partial(
            resolve_conflicting_options,
            old_option="my_opt",
            new_option="my_opt",
            old_scope="old-scope",
            new_scope="new-scope",
        )
        old_val = "ancient"
        new_val = "modern"
        old_default_rv = RankedValue(RankedValue.HARDCODED, old_val)
        new_default_rv = RankedValue(RankedValue.HARDCODED, new_val)
        old_configured_rv = RankedValue(RankedValue.FLAG, old_val)
        new_configured_rv = RankedValue(RankedValue.FLAG, new_val)

        def assert_option_resolved(
            *,
            old_configured: bool = False,
            new_configured: bool = False,
            expected: str,
        ) -> None:
            old_container, new_container = OptionValueContainer(
            ), OptionValueContainer()
            old_container.my_opt = old_configured_rv if old_configured else old_default_rv
            new_container.my_opt = new_configured_rv if new_configured else new_default_rv
            assert resolve_options(old_container=old_container,
                                   new_container=new_container) == expected

        assert_option_resolved(expected=new_val)
        assert_option_resolved(old_configured=True, expected=old_val)
        assert_option_resolved(new_configured=True, expected=new_val)

        # both configured -> raise an error
        old_container, new_container = OptionValueContainer(
        ), OptionValueContainer()
        old_container.my_opt = old_configured_rv
        new_container.my_opt = new_configured_rv
        with pytest.raises(ValueError) as e:
            resolve_options(old_container=old_container,
                            new_container=new_container)
        assert "--old-scope-my-opt" in str(e.value)
        assert "--new-scope-my-opt" in str(e.value)
Example #19
0
 def register(*args, **kwargs):
   default = kwargs.get('default')
   if default is None:
     action = kwargs.get('action')
     if action == 'store_true':
       default = False
     if action == 'append':
       default = []
   for flag_name in args:
     normalized_flag_name = flag_name.lstrip('-').replace('-', '_')
     defaults[normalized_flag_name] = RankedValue(RankedValue.HARDCODED, default)
Example #20
0
def test_find_python_interpreter_constraints_from_lockfile() -> None:
    def assert_ics(
        lockfile: str,
        expected: list[str],
        *,
        ics: RankedValue = RankedValue(Rank.HARDCODED,
                                       Black.default_interpreter_constraints),
        metadata: PythonLockfileMetadata
        | None = PythonLockfileMetadata.new(
            InterpreterConstraints(["==2.7.*"]), set()),
    ) -> None:
        black = create_subsystem(
            Black,
            lockfile=lockfile,
            interpreter_constraints=ics,
            version="v",
            extra_requirements=[],
        )
        loaded_lock = LoadedLockfile(
            EMPTY_DIGEST,
            "black.lock",
            metadata=metadata,
            requirement_estimate=1,
            is_pex_native=True,
            constraints_strings=None,
            original_lockfile=Lockfile("black.lock",
                                       file_path_description_of_origin="foo",
                                       resolve_name="black"),
        )
        result = run_rule_with_mocks(
            _find_python_interpreter_constraints_from_lockfile,
            rule_args=[black],
            mock_gets=[
                MockGet(
                    output_type=LoadedLockfile,
                    input_type=LoadedLockfileRequest,
                    mock=lambda _: loaded_lock,
                )
            ],
        )
        assert result == InterpreterConstraints(expected)

    # If ICs are set by user, always use those.
    for lockfile in (NO_TOOL_LOCKFILE, DEFAULT_TOOL_LOCKFILE, "black.lock"):
        assert_ics(lockfile, ["==3.8.*"],
                   ics=RankedValue(Rank.CONFIG, ["==3.8.*"]))

    assert_ics(NO_TOOL_LOCKFILE, Black.default_interpreter_constraints)
    assert_ics(DEFAULT_TOOL_LOCKFILE, Black.default_interpreter_constraints)

    assert_ics("black.lock", ["==2.7.*"])
    assert_ics("black.lock",
               Black.default_interpreter_constraints,
               metadata=None)
Example #21
0
 def _support_current_pants_plugin_options_usage(self):
     export_options = self.get_options()
     try:
         ivy_options = self.context.options.for_scope('resolve.ivy')
     except OptionsError:
         # No resolve.ivy task installed, so continue silently.
         ivy_options = []
     for name in set.intersection(set(export_options), set(ivy_options)):
         if not ivy_options.is_default(name):
             setattr(export_options, name,
                     RankedValue(RankedValue.FLAG, ivy_options[name]))
Example #22
0
 def register_options(cls, register):
   super(ExplainOptionsTask, cls).register_options(register)
   register('--scope', help='Only show options in this scope.')
   register('--name', help='Only show options with this name.')
   register('--rank', choices=RankedValue.get_names(),
            help='Only show options with at least this importance.')
   register('--show-history', type=bool,
            help='Show the previous values options had before being overridden.')
   register('--only-overridden', type=bool,
            help='Only show values that overrode defaults.')
   register('--skip-inherited', type=bool, default=True,
            help='Do not show inherited options, unless their values differ from their parents.')
Example #23
0
    def test_indexing(self) -> None:
        o = OptionValueContainer()
        o.foo = RankedValue(RankedValue.CONFIG, 1)
        self.assertEqual(1, o["foo"])

        self.assertEqual(1, o.get("foo"))
        self.assertEqual(1, o.get("foo", 2))
        self.assertIsNone(o.get("unknown"))
        self.assertEqual(2, o.get("unknown", 2))

        with self.assertRaises(AttributeError):
            o["bar"]
Example #24
0
    def test_indexing(self):
        o = OptionValueContainer()
        o.foo = RankedValue(RankedValue.CONFIG, 1)
        self.assertEqual(1, o['foo'])

        self.assertEqual(1, o.get('foo'))
        self.assertEqual(1, o.get('foo', 2))
        self.assertIsNone(o.get('unknown'))
        self.assertEqual(2, o.get('unknown', 2))

        with self.assertRaises(AttributeError):
            o['bar']
Example #25
0
 def test_invalidation_for_global_constraints(self):
     # Because the system is setup with interpreter constraints, the task should
     # invalidate on the first run.
     self._select_interpreter_and_get_version([self.tgt1], should_invalidate=True)
     self.set_options_for_scope(
         PythonSetup.options_scope,
         interpreter_constraints=RankedValue(RankedValue.CONFIG, ["IronPython>2.77.777"]),
     )
     # After changing the global interpreter constraints, the task should invalidate.
     self._select_interpreter_and_get_version([self.tgt1], should_invalidate=True)
     # If the global constraints don't change, the task should not invalidate.
     self._select_interpreter_and_get_version([self.tgt1], should_invalidate=False)
Example #26
0
def test_is_python2(constraints, compatibilities):
    Subsystem.reset()
    init_subsystem(
        PythonSetup,
        {
            PythonSetup.options_scope: {
                "interpreter_constraints": RankedValue(Rank.CONFIG,
                                                       constraints)
            }
        },
    )
    assert is_python2(compatibilities, PythonSetup.global_instance())
Example #27
0
    def setUp(self):
        super(SelectInterpreterTest, self).setUp()

        # We're tied tightly to pex implementation details here faking out a python binary that outputs
        # only one value no matter what arguments, environment or input stream it has attached. That
        # value is the interpreter identity which is - minimally, one line containing:
        # <impl> <abi> <impl_version> <major> <minor> <patch>

        def fake_interpreter(id_str):
            interpreter_dir = safe_mkdtemp()
            binary = os.path.join(interpreter_dir, 'python')
            with open(binary, 'w') as fp:
                fp.write(
                    dedent("""
        #!{}
        from __future__ import print_function

        print({!r})
        """.format(PythonInterpreter.get().binary, id_str)).strip())
            chmod_plus_x(binary)
            return PythonInterpreter.from_binary(binary)

        # impl, abi, impl_version, major, minor, patch
        self.fake_interpreters = [
            fake_interpreter('ip ip2 2 2 77 777'),
            fake_interpreter('ip ip2 2 2 88 888'),
            fake_interpreter('ip ip2 2 2 99 999')
        ]

        self.set_options_for_scope(
            PythonSetup.options_scope,
            interpreter_constraints=RankedValue(RankedValue.CONFIG,
                                                ['IronPython>=2.55']),
            interpreter_search_paths=[
                interpreter.binary for interpreter in self.fake_interpreters
            ])

        self.reqtgt = self.make_target(
            spec='req',
            target_type=PythonRequirementLibrary,
            requirements=[],
        )
        self.tgt1 = self._fake_target('tgt1')
        self.tgt2 = self._fake_target('tgt2',
                                      compatibility=['IronPython>2.77.777'])
        self.tgt3 = self._fake_target('tgt3',
                                      compatibility=['IronPython>2.88.888'])
        self.tgt4 = self._fake_target('tgt4',
                                      compatibility=['IronPython<2.99.999'])
        self.tgt20 = self._fake_target('tgt20', dependencies=[self.tgt2])
        self.tgt30 = self._fake_target('tgt30', dependencies=[self.tgt3])
        self.tgt40 = self._fake_target('tgt40', dependencies=[self.tgt4])
Example #28
0
    def test_indexing(self) -> None:
        ob = OptionValueContainerBuilder()
        ob.foo = RankedValue(Rank.CONFIG, 1)
        o = ob.build()

        assert 1 == o["foo"]
        assert 1 == o.get("foo")
        assert 1 == o.get("foo", 2)
        assert o.get("unknown") is None
        assert 2 == o.get("unknown", 2)

        with pytest.raises(AttributeError):
            o["bar"]
Example #29
0
  def _compute_default(self, dest, kwargs):
    """Compute the default value to use for an option's registration.

    The source of the default value is chosen according to the ranking in RankedValue.
    """
    config_section = 'DEFAULT' if self._scope == GLOBAL_SCOPE else self._scope
    env_var = 'PANTS_{0}_{1}'.format(config_section.upper().replace('.', '_'), dest.upper())
    value_type = kwargs.get('type', str)
    env_val_str = self._env.get(env_var) if self._env else None

    env_val = None if env_val_str is None else value_type(env_val_str)
    config_val = self._config.get(config_section, dest, default=None) if self._config else None
    hardcoded_val = kwargs.get('default')
    return RankedValue.choose(None, env_val, config_val, hardcoded_val)
Example #30
0
 def _format_record(self, record):
     simple_rank = RankedValue.get_rank_name(record.rank)
     if self.is_json():
         return record.value, simple_rank
     elif self.is_text():
         simple_value = str(record.value)
         value_color = green if self.get_options().colors else lambda x: x
         formatted_value = value_color(simple_value)
         rank_color = self._rank_color(record.rank)
         formatted_rank = "(from {rank}{details})".format(
             rank=simple_rank,
             details=rank_color(" {}".format(record.details)) if record.details else "",
         )
         return "{value} {rank}".format(value=formatted_value, rank=formatted_rank,)
Example #31
0
 def normalize_kwargs(orig_args, orig_kwargs):
     nkwargs = copy.copy(orig_kwargs)
     dest = self.parse_dest(*orig_args, **nkwargs)
     nkwargs["dest"] = dest
     if not ("default" in nkwargs and isinstance(nkwargs["default"], RankedValue)):
         type_arg = nkwargs.get("type", str)
         member_type = nkwargs.get("member_type", str)
         default_val = self.to_value_type(
             nkwargs.get("default"), type_arg, member_type, dest
         )
         if isinstance(default_val, (ListValueComponent, DictValueComponent)):
             default_val = default_val.val
         nkwargs["default"] = RankedValue(Rank.HARDCODED, default_val)
     return nkwargs
Example #32
0
 def register_options(cls, register):
   super(ExplainOptionsTask, cls).register_options(register)
   register('--scope', help='Only show options in this scope. Use GLOBAL for global scope.')
   register('--name', help='Only show options with this name.')
   register('--rank', choices=RankedValue.get_names(),
            help='Only show options with at least this importance.')
   register('--show-history', type=bool,
            help='Show the previous values options had before being overridden.')
   register('--only-overridden', type=bool,
            help='Only show values that overrode defaults.')
   register('--skip-inherited', type=bool, default=True,
            help='Do not show inherited options, unless their values differ from their parents.')
   register('--output-format', choices=['text', 'json'], default='text',
            help='Specify the format options will be printed.')
Example #33
0
def test_resolve_conflicting_options() -> None:
    old_val = "ancient"
    new_val = "modern"
    old_default_rv = RankedValue(Rank.HARDCODED, old_val)
    new_default_rv = RankedValue(Rank.HARDCODED, new_val)
    old_configured_rv = RankedValue(Rank.FLAG, old_val)
    new_configured_rv = RankedValue(Rank.FLAG, new_val)

    def option_resolved(*,
                        old_configured: bool = False,
                        new_configured: bool = False):
        old_container_builder, new_container_builder = (
            OptionValueContainerBuilder(),
            OptionValueContainerBuilder(),
        )
        old_container_builder.my_opt = old_configured_rv if old_configured else old_default_rv
        new_container_builder.my_opt = new_configured_rv if new_configured else new_default_rv
        old_container = old_container_builder.build()
        new_container = new_container_builder.build()
        return resolve_conflicting_options(
            old_option="my_opt",
            new_option="my_opt",
            old_scope="old-scope",
            new_scope="new-scope",
            old_container=old_container,
            new_container=new_container,
        )

    assert option_resolved() == new_val
    assert option_resolved(old_configured=True) == old_val
    assert option_resolved(new_configured=True) == new_val

    # both configured -> raise an error
    with pytest.raises(ValueError) as e:
        option_resolved(old_configured=True, new_configured=True)
    assert "--old-scope-my-opt" in str(e.value)
    assert "--new-scope-my-opt" in str(e.value)
 def _format_record(self, record):
   simple_rank = RankedValue.get_rank_name(record.rank)
   if self.is_json():
     return record.value, simple_rank
   elif self.is_text():
     simple_value = str(record.value)
     value_color = green if self.get_options().colors else lambda x: x
     formatted_value = value_color(simple_value)
     rank_color = self._rank_color(record.rank)
     formatted_rank = '(from {rank}{details})'.format(
       rank=simple_rank,
       details=rank_color(' {}'.format(record.details)) if record.details else '',
     )
     return '{value} {rank}'.format(
       value=formatted_value,
       rank=formatted_rank,
     )
Example #35
0
  def _compute_default(self, dest, kwargs):
    """Compute the default value to use for an option's registration.

    The source of the default value is chosen according to the ranking in RankedValue.
    """
    config_section = 'DEFAULT' if self._scope == GLOBAL_SCOPE else self._scope
    udest = dest.upper()
    if self._scope == GLOBAL_SCOPE:
      # For convenience, we allow three forms of env var for global scope options.
      # The fully-specified env var is PANTS_DEFAULT_FOO, which is uniform with PANTS_<SCOPE>_FOO
      # for all the other scopes.  However we also allow simply PANTS_FOO. And if the option name
      # itself starts with 'pants-' then we also allow simply FOO. E.g., PANTS_WORKDIR instead of
      # PANTS_PANTS_WORKDIR or PANTS_DEFAULT_PANTS_WORKDIR. We take the first specified value we
      # find, in this order: PANTS_DEFAULT_FOO, PANTS_FOO, FOO.
      env_vars = ['PANTS_DEFAULT_{0}'.format(udest), 'PANTS_{0}'.format(udest)]
      if udest.startswith('PANTS_'):
        env_vars.append(udest)
    else:
      sanitized_env_var_scope = self._ENV_SANITIZER_RE.sub('_', config_section.upper())
      env_vars = ['PANTS_{0}_{1}'.format(sanitized_env_var_scope, udest)]
    value_type = self.str_to_bool if is_boolean_flag(kwargs) else kwargs.get('type', str)
    env_val_str = None
    if self._env:
      for env_var in env_vars:
        if env_var in self._env:
          env_val_str = self._env.get(env_var)
          break
    env_val = None if env_val_str is None else value_type(env_val_str)
    if kwargs.get('action') == 'append':
      config_val_strs = self._config.getlist(config_section, dest) if self._config else None
      config_val = (None if config_val_strs is None else
                    [value_type(config_val_str) for config_val_str in config_val_strs])
      default = []
    else:
      config_val_str = (self._config.get(config_section, dest, default=None)
                        if self._config else None)
      config_val = None if config_val_str is None else value_type(config_val_str)
      default = None
    hardcoded_val = kwargs.get('default')
    return RankedValue.choose(None, env_val, config_val, hardcoded_val, default)
 def _rank_filter(self, rank):
   pattern = self.get_options().rank
   if not pattern:
     return True
   return rank >= RankedValue.get_rank_value(pattern)
Example #37
0
  def _compute_value(self, dest, kwargs, flag_vals):
    """Compute the value to use for an option.

    The source of the default value is chosen according to the ranking in RankedValue.
    """
    is_fromfile = kwargs.get('fromfile', False)
    action = kwargs.get('action')
    if is_fromfile and action and action != 'append':
      raise ParseError('Cannot fromfile {} with an action ({}) in scope {}'
                       .format(dest, action, self._scope))

    config_section = GLOBAL_SCOPE_CONFIG_SECTION if self._scope == GLOBAL_SCOPE else self._scope
    udest = dest.upper()
    if self._scope == GLOBAL_SCOPE:
      # For convenience, we allow three forms of env var for global scope options.
      # The fully-specified env var is PANTS_DEFAULT_FOO, which is uniform with PANTS_<SCOPE>_FOO
      # for all the other scopes.  However we also allow simply PANTS_FOO. And if the option name
      # itself starts with 'pants-' then we also allow simply FOO. E.g., PANTS_WORKDIR instead of
      # PANTS_PANTS_WORKDIR or PANTS_DEFAULT_PANTS_WORKDIR. We take the first specified value we
      # find, in this order: PANTS_DEFAULT_FOO, PANTS_FOO, FOO.
      env_vars = ['PANTS_DEFAULT_{0}'.format(udest), 'PANTS_{0}'.format(udest)]
      if udest.startswith('PANTS_'):
        env_vars.append(udest)
    else:
      sanitized_env_var_scope = self._ENV_SANITIZER_RE.sub('_', config_section.upper())
      env_vars = ['PANTS_{0}_{1}'.format(sanitized_env_var_scope, udest)]

    value_type = self.str_to_bool if is_boolean_flag(kwargs) else kwargs.get('type', str)

    env_val_str = None
    if self._env:
      for env_var in env_vars:
        if env_var in self._env:
          env_val_str = self._env.get(env_var)
          break

    config_val_str = self._config.get(config_section, dest, default=None)
    config_source_file = self._config.get_source_for_option(config_section, dest)
    if config_source_file is not None:
      config_source_file = os.path.relpath(config_source_file)

    def expand(val_str):
      if is_fromfile and val_str and val_str.startswith('@') and not val_str.startswith('@@'):
        fromfile = val_str[1:]
        try:
          with open(fromfile) as fp:
            return fp.read().strip()
        except IOError as e:
          raise self.FromfileError('Failed to read {} in {} from file {}: {}'.format(
            dest, self._scope_str(), fromfile, e))
      else:
        # Support a literal @ for fromfile values via @@.
        return val_str[1:] if is_fromfile and val_str.startswith('@@') else val_str

    def parse_typed_list(val_str):
      return None if val_str is None else [value_type(x) for x in list_option(expand(val_str))]

    def parse_typed_item(val_str):
      return None if val_str is None else value_type(expand(val_str))

    flag_val = None
    if flag_vals:
      if action == 'append':
        flag_val = [parse_typed_item(v) for v in flag_vals]
      elif len(flag_vals) > 1:
        raise ParseError('Multiple cmd line flags specified for option {} in {}'.format(
          dest, self._scope_str()))
      else:
        flag_val = parse_typed_item(flag_vals[0])

    default, parse = ([], parse_typed_list) if action == 'append' else (None, parse_typed_item)

    config_val = parse(config_val_str)
    env_val = parse(env_val_str)
    hardcoded_val = kwargs.get('default')

    config_details = 'in {}'.format(config_source_file) if config_source_file else None

    # Note: ranked_vals is guaranteed to have at least one element, and none of the values
    # of any of its elements will be None.
    ranked_vals = list(reversed(list(RankedValue.prioritized_iter(
      flag_val, env_val, config_val, hardcoded_val, default))))
    choices = kwargs.get('choices')
    for ranked_val in ranked_vals:
      details = config_details if ranked_val.rank == RankedValue.CONFIG else None
      self._option_tracker.record_option(scope=self._scope,
                                         option=dest,
                                         value=ranked_val.value,
                                         rank=ranked_val.rank,
                                         deprecation_version=kwargs.get('deprecated_version'),
                                         details=details)

    def check(val):
      if choices is not None and val is not None and val not in choices:
        raise ParseError('{} is not an allowed value for option {} in {}. '
                         'Must be one of: {}'.format(
          val, dest, self._scope_str(), choices
        ))
      return val

    if action == 'append':
      merged_rank = ranked_vals[-1].rank
      merged_val = [check(val) for vals in ranked_vals for val in vals.value]
      return RankedValue(merged_rank, merged_val)
    else:
      map(lambda rv: check(rv.value), ranked_vals)
      return ranked_vals[-1]
Example #38
0
  def _compute_value(self, dest, kwargs, flag_val_strs):
    """Compute the value to use for an option.

    The source of the default value is chosen according to the ranking in RankedValue.
    """
    # Helper function to convert a string to a value of the option's type.
    def to_value_type(val_str):
      if val_str is None:
        return None
      elif kwargs.get('type') == bool:
        return self._ensure_bool(val_str)
      else:
        return self._wrap_type(kwargs.get('type', str))(val_str)

    # Helper function to expand a fromfile=True value string, if needed.
    def expand(val_str):
      if kwargs.get('fromfile', False) and val_str and val_str.startswith('@'):
        if val_str.startswith('@@'):   # Support a literal @ for fromfile values via @@.
          return val_str[1:]
        else:
          fromfile = val_str[1:]
          try:
            with open(fromfile) as fp:
              return fp.read().strip()
          except IOError as e:
            raise self.FromfileError('Failed to read {} in {} from file {}: {}'.format(
                dest, self._scope_str(), fromfile, e))
      else:
        return val_str

    # Get value from config files, and capture details about its derivation.
    config_details = None
    config_section = GLOBAL_SCOPE_CONFIG_SECTION if self._scope == GLOBAL_SCOPE else self._scope
    config_val_str = expand(self._config.get(config_section, dest, default=None))
    config_source_file = self._config.get_source_for_option(config_section, dest)
    if config_source_file is not None:
      config_source_file = os.path.relpath(config_source_file)
      config_details = 'in {}'.format(config_source_file)

    # Get value from environment, and capture details about its derivation.
    udest = dest.upper()
    if self._scope == GLOBAL_SCOPE:
      # For convenience, we allow three forms of env var for global scope options.
      # The fully-specified env var is PANTS_GLOBAL_FOO, which is uniform with PANTS_<SCOPE>_FOO
      # for all the other scopes.  However we also allow simply PANTS_FOO. And if the option name
      # itself starts with 'pants-' then we also allow simply FOO. E.g., PANTS_WORKDIR instead of
      # PANTS_PANTS_WORKDIR or PANTS_GLOBAL_PANTS_WORKDIR. We take the first specified value we
      # find, in this order: PANTS_GLOBAL_FOO, PANTS_FOO, FOO.
      env_vars = ['PANTS_GLOBAL_{0}'.format(udest), 'PANTS_{0}'.format(udest)]
      if udest.startswith('PANTS_'):
        env_vars.append(udest)
    else:
      sanitized_env_var_scope = self._ENV_SANITIZER_RE.sub('_', self._scope.upper())
      env_vars = ['PANTS_{0}_{1}'.format(sanitized_env_var_scope, udest)]

    env_val_str = None
    env_details = None
    if self._env:
      for env_var in env_vars:
        if env_var in self._env:
          env_val_str = expand(self._env.get(env_var))
          env_details = 'from env var {}'.format(env_var)
          break

    # Get value from cmd-line flags.
    flag_vals = [to_value_type(expand(x)) for x in flag_val_strs]
    if is_list_option(kwargs):
      # Note: It's important to set flag_val to None if no flags were specified, so we can
      # distinguish between no flags set vs. explicit setting of the value to [].
      flag_val = ListValueComponent.merge(flag_vals) if flag_vals else None
    elif len(flag_vals) > 1:
      raise ParseError('Multiple cmd line flags specified for option {} in {}'.format(
          dest, self._scope_str()))
    elif len(flag_vals) == 1:
      flag_val = flag_vals[0]
    else:
      flag_val = None

    # Rank all available values.
    # Note that some of these values may already be of the value type, but type conversion
    # is idempotent, so this is OK.

    values_to_rank = [to_value_type(x) for x in
                      [flag_val, env_val_str, config_val_str, kwargs.get('default'), None]]
    # Note that ranked_vals will always have at least one element, and all elements will be
    # instances of RankedValue (so none will be None, although they may wrap a None value).
    ranked_vals = list(reversed(list(RankedValue.prioritized_iter(*values_to_rank))))

    # Record info about the derivation of each of the values.
    for ranked_val in ranked_vals:
      if ranked_val.rank == RankedValue.CONFIG:
        details = config_details
      elif ranked_val.rank == RankedValue.ENVIRONMENT:
        details = env_details
      else:
        details = None
      self._option_tracker.record_option(scope=self._scope,
                                         option=dest,
                                         value=ranked_val.value,
                                         rank=ranked_val.rank,
                                         deprecation_version=kwargs.get('removal_version'),
                                         details=details)

    # Helper function to check various validity constraints on final option values.
    def check(val):
      if val is not None:
        choices = kwargs.get('choices')
        if choices is not None and val not in choices:
          raise ParseError('{} is not an allowed value for option {} in {}. '
                           'Must be one of: {}'.format(val, dest, self._scope_str(), choices))
        elif kwargs.get('type') == file_option and not os.path.isfile(val):
          raise ParseError('File value {} for option {} in {} does not exist.'.format(
              val, dest, self._scope_str()))

    # Generate the final value from all available values, and check that it (or its members,
    # if a list) are in the set of allowed choices.
    if is_list_option(kwargs):
      merged_rank = ranked_vals[-1].rank
      merged_val = ListValueComponent.merge(
          [rv.value for rv in ranked_vals if rv.value is not None]).val
      merged_val = [self._wrap_type(kwargs.get('member_type', str))(x) for x in merged_val]
      map(check, merged_val)
      ret = RankedValue(merged_rank, merged_val)
    else:
      ret = ranked_vals[-1]
      check(ret.value)

    # All done!
    return ret
Example #39
0
  def _compute_default(self, dest, kwargs):
    """Compute the default value to use for an option's registration.

    The source of the default value is chosen according to the ranking in RankedValue.
    """
    is_fromfile = kwargs.get('fromfile', False)
    action = kwargs.get('action')
    if is_fromfile and action:
      raise ParseError('Cannot fromfile {} with an action ({}) in scope {}'
                       .format(dest, action, self._scope))

    config_section = 'DEFAULT' if self._scope == GLOBAL_SCOPE else self._scope
    udest = dest.upper()
    if self._scope == GLOBAL_SCOPE:
      # For convenience, we allow three forms of env var for global scope options.
      # The fully-specified env var is PANTS_DEFAULT_FOO, which is uniform with PANTS_<SCOPE>_FOO
      # for all the other scopes.  However we also allow simply PANTS_FOO. And if the option name
      # itself starts with 'pants-' then we also allow simply FOO. E.g., PANTS_WORKDIR instead of
      # PANTS_PANTS_WORKDIR or PANTS_DEFAULT_PANTS_WORKDIR. We take the first specified value we
      # find, in this order: PANTS_DEFAULT_FOO, PANTS_FOO, FOO.
      env_vars = ['PANTS_DEFAULT_{0}'.format(udest), 'PANTS_{0}'.format(udest)]
      if udest.startswith('PANTS_'):
        env_vars.append(udest)
    else:
      sanitized_env_var_scope = self._ENV_SANITIZER_RE.sub('_', config_section.upper())
      env_vars = ['PANTS_{0}_{1}'.format(sanitized_env_var_scope, udest)]

    value_type = self.str_to_bool if is_boolean_flag(kwargs) else kwargs.get('type', str)

    env_val_str = None
    if self._env:
      for env_var in env_vars:
        if env_var in self._env:
          env_val_str = self._env.get(env_var)
          break

    def expand(val_str):
      if is_fromfile and val_str and val_str.startswith('@') and not val_str.startswith('@@'):
        fromfile = val_str[1:]
        try:
          with open(fromfile) as fp:
            return fp.read().strip()
        except IOError as e:
          raise self.FromfileError('Failed to read {} from file {}: {}'.format(dest, fromfile, e))
      else:
        # Support a literal @ for fromfile values via @@.
        return val_str[1:] if is_fromfile and val_str.startswith('@@') else val_str

    if is_fromfile:
      kwargs['type'] = lambda flag_val_str: value_type(expand(flag_val_str))  # Expand flag values.

    env_val = None if env_val_str is None else value_type(expand(env_val_str))  # Expand env values.

    config_val = None
    if action == 'append':
      config_val_strs = self._config.getlist(config_section, dest) if self._config else None
      if config_val_strs is not None:
        config_val = [value_type(config_val_str) for config_val_str in config_val_strs]
      default = []
    else:
      config_val_str = (self._config.get(config_section, dest, default=None)
                        if self._config else None)
      if config_val_str is not None:
        config_val = value_type(expand(config_val_str))  # Expand config values.
      default = None

    hardcoded_val = kwargs.get('default')  # We don't expand hard coded defaults.
    return RankedValue.choose(None, env_val, config_val, hardcoded_val, default)
Example #40
0
  def _compute_value(self, dest, kwargs, flag_val_strs):
    """Compute the value to use for an option.

    The source of the default value is chosen according to the ranking in RankedValue.
    """
    # Helper function to convert a string to a value of the option's type.
    def to_value_type(val_str):
      if val_str is None:
        return None
      elif kwargs.get('type') == bool:
        return self._ensure_bool(val_str)
      else:
        type_arg = kwargs.get('type', str)
        try:
          return self._wrap_type(type_arg)(val_str)
        except TypeError as e:
          raise ParseError(
            "Error applying type '{}' to option value '{}', for option '--{}' in {}: {}"
            .format(type_arg.__name__, val_str, dest, self._scope_str(), e))

    # Helper function to expand a fromfile=True value string, if needed.
    # May return a string or a dict/list decoded from a json/yaml file.
    def expand(val_or_str):
      if (kwargs.get('fromfile', True) and val_or_str and
          isinstance(val_or_str, str) and val_or_str.startswith('@')):
        if val_or_str.startswith('@@'):   # Support a literal @ for fromfile values via @@.
          return val_or_str[1:]
        else:
          fromfile = val_or_str[1:]
          try:
            with open(fromfile, 'r') as fp:
              s = fp.read().strip()
              if fromfile.endswith('.json'):
                return json.loads(s)
              elif fromfile.endswith('.yml') or fromfile.endswith('.yaml'):
                return yaml.safe_load(s)
              else:
                return s
          except (IOError, ValueError, yaml.YAMLError) as e:
            raise self.FromfileError('Failed to read {} in {} from file {}: {}'.format(
                dest, self._scope_str(), fromfile, e))
      else:
        return val_or_str

    # Get value from config files, and capture details about its derivation.
    config_details = None
    config_section = GLOBAL_SCOPE_CONFIG_SECTION if self._scope == GLOBAL_SCOPE else self._scope
    config_default_val_or_str = expand(self._config.get(Config.DEFAULT_SECTION, dest, default=None))
    config_val_or_str = expand(self._config.get(config_section, dest, default=None))
    config_source_file = (self._config.get_source_for_option(config_section, dest) or
        self._config.get_source_for_option(Config.DEFAULT_SECTION, dest))
    if config_source_file is not None:
      config_source_file = os.path.relpath(config_source_file)
      config_details = 'in {}'.format(config_source_file)

    # Get value from environment, and capture details about its derivation.
    udest = dest.upper()
    if self._scope == GLOBAL_SCOPE:
      # For convenience, we allow three forms of env var for global scope options.
      # The fully-specified env var is PANTS_GLOBAL_FOO, which is uniform with PANTS_<SCOPE>_FOO
      # for all the other scopes.  However we also allow simply PANTS_FOO. And if the option name
      # itself starts with 'pants-' then we also allow simply FOO. E.g., PANTS_WORKDIR instead of
      # PANTS_PANTS_WORKDIR or PANTS_GLOBAL_PANTS_WORKDIR. We take the first specified value we
      # find, in this order: PANTS_GLOBAL_FOO, PANTS_FOO, FOO.
      env_vars = ['PANTS_GLOBAL_{0}'.format(udest), 'PANTS_{0}'.format(udest)]
      if udest.startswith('PANTS_'):
        env_vars.append(udest)
    else:
      sanitized_env_var_scope = self._ENV_SANITIZER_RE.sub('_', self._scope.upper())
      env_vars = ['PANTS_{0}_{1}'.format(sanitized_env_var_scope, udest)]

    env_val_or_str = None
    env_details = None
    if self._env:
      for env_var in env_vars:
        if env_var in self._env:
          env_val_or_str = expand(self._env.get(env_var))
          env_details = 'from env var {}'.format(env_var)
          break

    # Get value from cmd-line flags.
    flag_vals = [to_value_type(expand(x)) for x in flag_val_strs]
    if is_list_option(kwargs):
      # Note: It's important to set flag_val to None if no flags were specified, so we can
      # distinguish between no flags set vs. explicit setting of the value to [].
      flag_val = ListValueComponent.merge(flag_vals) if flag_vals else None
    elif is_dict_option(kwargs):
      # Note: It's important to set flag_val to None if no flags were specified, so we can
      # distinguish between no flags set vs. explicit setting of the value to {}.
      flag_val = DictValueComponent.merge(flag_vals) if flag_vals else None
    elif len(flag_vals) > 1:
      raise ParseError('Multiple cmd line flags specified for option {} in {}'.format(
          dest, self._scope_str()))
    elif len(flag_vals) == 1:
      flag_val = flag_vals[0]
    else:
      flag_val = None

    # Rank all available values.
    # Note that some of these values may already be of the value type, but type conversion
    # is idempotent, so this is OK.

    values_to_rank = [to_value_type(x) for x in
                      [flag_val, env_val_or_str, config_val_or_str,
                       config_default_val_or_str, kwargs.get('default'), None]]
    # Note that ranked_vals will always have at least one element, and all elements will be
    # instances of RankedValue (so none will be None, although they may wrap a None value).
    ranked_vals = list(reversed(list(RankedValue.prioritized_iter(*values_to_rank))))

    def record_option(value, rank, option_details=None):
      deprecation_version = kwargs.get('removal_version')
      self._option_tracker.record_option(scope=self._scope,
                                         option=dest,
                                         value=value,
                                         rank=rank,
                                         deprecation_version=deprecation_version,
                                         details=option_details)

    # Record info about the derivation of each of the contributing values.
    detail_history = []
    for ranked_val in ranked_vals:
      if ranked_val.rank in (RankedValue.CONFIG, RankedValue.CONFIG_DEFAULT):
        details = config_details
      elif ranked_val.rank == RankedValue.ENVIRONMENT:
        details = env_details
      else:
        details = None
      if details:
        detail_history.append(details)
      record_option(value=ranked_val.value, rank=ranked_val.rank, option_details=details)

    # Helper function to check various validity constraints on final option values.
    def check(val):
      if val is not None:
        choices = kwargs.get('choices')
        # If the `type` argument has an `all_variants` attribute, use that as `choices` if not
        # already set. Using an attribute instead of checking a subclass allows `type` arguments
        # which are functions to have an implicit fallback `choices` set as well.
        if choices is None and 'type' in kwargs:
          type_arg = kwargs.get('type')
          if hasattr(type_arg, 'all_variants'):
            choices = list(type_arg.all_variants)
        # TODO: convert this into an enum() pattern match!
        if choices is not None and val not in choices:
          raise ParseError('`{}` is not an allowed value for option {} in {}. '
                           'Must be one of: {}'.format(val, dest, self._scope_str(), choices))
        elif kwargs.get('type') == dir_option and not os.path.isdir(val):
          raise ParseError('Directory value `{}` for option {} in {} does not exist.'.format(
              val, dest, self._scope_str()))
        elif kwargs.get('type') == file_option and not os.path.isfile(val):
          raise ParseError('File value `{}` for option {} in {} does not exist.'.format(
              val, dest, self._scope_str()))

    # Generate the final value from all available values, and check that it (or its members,
    # if a list) are in the set of allowed choices.
    if is_list_option(kwargs):
      merged_rank = ranked_vals[-1].rank
      merged_val = ListValueComponent.merge(
          [rv.value for rv in ranked_vals if rv.value is not None]).val
      # TODO: run `check()` for all elements of a list option too!!!
      merged_val = [self._convert_member_type(kwargs.get('member_type', str), x)
                    for x in merged_val]
      for val in merged_val:
        check(val)
      ret = RankedValue(merged_rank, merged_val)
    elif is_dict_option(kwargs):
      # TODO: convert `member_type` for dict values too!
      merged_rank = ranked_vals[-1].rank
      merged_val = DictValueComponent.merge(
          [rv.value for rv in ranked_vals if rv.value is not None]).val
      for val in merged_val:
        check(val)
      ret = RankedValue(merged_rank, merged_val)
    else:
      ret = ranked_vals[-1]
      check(ret.value)

    # Record info about the derivation of the final value.
    merged_details = ', '.join(detail_history) if detail_history else None
    record_option(value=ret.value, rank=ret.rank, option_details=merged_details)

    # All done!
    return ret
Example #41
0
  def _compute_default(self, kwargs):
    """Compute the default value to use for an option's registration.

    The source of the default value is chosen according to the ranking in RankedValue.

    Note: Only call if kwargs has a 'dest' key set.
    """
    dest = kwargs['dest']
    is_fromfile = kwargs.get('fromfile', False)
    action = kwargs.get('action')
    if is_fromfile and action and action != 'append':
      raise ParseError('Cannot fromfile {} with an action ({}) in scope {}'
                       .format(dest, action, self._scope))

    config_section = 'DEFAULT' if self._scope == GLOBAL_SCOPE else self._scope
    udest = dest.upper()
    if self._scope == GLOBAL_SCOPE:
      # For convenience, we allow three forms of env var for global scope options.
      # The fully-specified env var is PANTS_DEFAULT_FOO, which is uniform with PANTS_<SCOPE>_FOO
      # for all the other scopes.  However we also allow simply PANTS_FOO. And if the option name
      # itself starts with 'pants-' then we also allow simply FOO. E.g., PANTS_WORKDIR instead of
      # PANTS_PANTS_WORKDIR or PANTS_DEFAULT_PANTS_WORKDIR. We take the first specified value we
      # find, in this order: PANTS_DEFAULT_FOO, PANTS_FOO, FOO.
      env_vars = ['PANTS_DEFAULT_{0}'.format(udest), 'PANTS_{0}'.format(udest)]
      if udest.startswith('PANTS_'):
        env_vars.append(udest)
    else:
      sanitized_env_var_scope = self._ENV_SANITIZER_RE.sub('_', config_section.upper())
      env_vars = ['PANTS_{0}_{1}'.format(sanitized_env_var_scope, udest)]

    value_type = self.str_to_bool if is_boolean_flag(kwargs) else kwargs.get('type', str)

    env_val_str = None
    if self._env:
      for env_var in env_vars:
        if env_var in self._env:
          env_val_str = self._env.get(env_var)
          break

    config_val_str = self._config.get(config_section, dest, default=None)
    config_source_file = self._config.get_source_for_option(config_section, dest)
    if config_source_file is not None:
      config_source_file = os.path.relpath(config_source_file)

    def expand(val_str):
      if is_fromfile and val_str and val_str.startswith('@') and not val_str.startswith('@@'):
        fromfile = val_str[1:]
        try:
          with open(fromfile) as fp:
            return fp.read().strip()
        except IOError as e:
          raise self.FromfileError('Failed to read {} from file {}: {}'.format(dest, fromfile, e))
      else:
        # Support a literal @ for fromfile values via @@.
        return val_str[1:] if is_fromfile and val_str.startswith('@@') else val_str

    def parse_typed_list(val_str):
      return None if val_str is None else [value_type(x) for x in list_option(expand(val_str))]

    def parse_typed_item(val_str):
      return None if val_str is None else value_type(expand(val_str))

    # Handle the forthcoming conversions argparse will need to do by placing our parse hook - we
    # handle the conversions for env and config ourselves below.  Unlike the env and config
    # handling, `action='append'` does not need to be handled specially since appended flag values
    # come as single items' thus only `parse_typed_item` is ever needed for the flag value type
    # conversions.
    if is_fromfile:
      kwargs['type'] = parse_typed_item

    default, parse = ([], parse_typed_list) if action == 'append' else (None, parse_typed_item)
    config_val = parse(config_val_str)
    env_val = parse(env_val_str)
    hardcoded_val = kwargs.get('default')

    config_details = 'in {}'.format(config_source_file) if config_source_file else None

    choices = list(RankedValue.prioritized_iter(None, env_val, config_val, hardcoded_val, default))
    for choice in reversed(choices):
      details = config_details if choice.rank == RankedValue.CONFIG else None
      self._option_tracker.record_option(scope=self._scope, option=dest, value=choice.value,
                                         rank=choice.rank, details=details)

    return choices[0]