diff --git a/3rdparty/python/requirements.txt b/3rdparty/python/requirements.txt index 7ded6b7f558..e1bac9cb47f 100644 --- a/3rdparty/python/requirements.txt +++ b/3rdparty/python/requirements.txt @@ -25,6 +25,7 @@ requests[security]>=2.20.1 responses==0.10.4 setproctitle==1.1.10 setuptools==40.6.3 +toml==0.10.0 typing-extensions==3.7.4 wheel==0.31.1 www-authenticate==0.9.2 diff --git a/src/python/pants/base/build_environment.py b/src/python/pants/base/build_environment.py index ee44ca87bce..c0f54d56fd0 100644 --- a/src/python/pants/base/build_environment.py +++ b/src/python/pants/base/build_environment.py @@ -3,6 +3,7 @@ import logging import os +from pathlib import Path from typing import Optional from pants.base.build_root import BuildRoot @@ -51,7 +52,10 @@ def get_pants_configdir() -> str: def get_default_pants_config_file() -> str: - """Return the default location of the pants config file.""" + """Return the default location of the Pants config file.""" + default_toml = Path(get_buildroot(), "pants.toml") + if default_toml.is_file(): + return str(default_toml) return os.path.join(get_buildroot(), 'pants.ini') diff --git a/src/python/pants/option/BUILD b/src/python/pants/option/BUILD index ef678f24574..bead1327dbb 100644 --- a/src/python/pants/option/BUILD +++ b/src/python/pants/option/BUILD @@ -8,6 +8,7 @@ python_library( '3rdparty/python:python-Levenshtein', '3rdparty/python:PyYAML', '3rdparty/python:setuptools', + '3rdparty/python:toml', '3rdparty/python:typing-extensions', '3rdparty/python/twitter/commons:twitter.common.collections', 'src/python/pants/base:build_environment', diff --git a/src/python/pants/option/config.py b/src/python/pants/option/config.py index aa5ef5cd9a6..ea1a547ab7c 100644 --- a/src/python/pants/option/config.py +++ b/src/python/pants/option/config.py @@ -6,12 +6,15 @@ import io import itertools import os +import re from abc import ABC, abstractmethod from contextlib import contextmanager from dataclasses import dataclass from hashlib import sha1 +from pathlib import PurePath from typing import Any, ClassVar, Dict, List, Mapping, Optional, Sequence, Tuple, Union, cast +import toml from twitter.common.collections import OrderedSet from typing_extensions import Literal @@ -90,11 +93,19 @@ def _meta_load( content_digest = sha1(content).hexdigest() normalized_seed_values = cls._determine_seed_values(seed_values=seed_values) - ini_parser = configparser.ConfigParser(defaults=normalized_seed_values) - ini_parser.read_string(content.decode()) + config_values: _ConfigValues + if PurePath(config_path).suffix == ".toml": + toml_values = cast(Dict[str, Any], toml.loads(content.decode())) + toml_values["DEFAULT"] = {**normalized_seed_values, **toml_values.get("DEFAULT", {})} + config_values = _TomlValues(toml_values) + else: + ini_parser = configparser.ConfigParser(defaults=normalized_seed_values) + ini_parser.read_string(content.decode()) + config_values = _IniValues(ini_parser) + single_file_configs.append( _SingleFileConfig( - config_path=config_path, content_digest=content_digest, values=_IniValues(ini_parser), + config_path=config_path, content_digest=content_digest, values=config_values, ), ) return _ChainedConfig(tuple(reversed(single_file_configs))) @@ -182,10 +193,11 @@ def get_source_for_option(self, section: str, option: str) -> Optional[str]: class _ConfigValues(ABC): """Encapsulates resolving the actual config values specified by the user's config file. - Beyond providing better encapsulation, this allows us to support alternative config file formats - in the future if we ever decide to support formats other than INI. + Due to encapsulation, this allows us to support both TOML and INI config files without any of + the rest of the Pants codebase knowing whether the config came from TOML or INI. """ + @property @abstractmethod def sections(self) -> List[str]: """Returns the sections in this config (not including DEFAULT).""" @@ -206,8 +218,9 @@ def get_value(self, section: str, option: str) -> Optional[str]: def options(self, section: str) -> List[str]: """All options defined for the section.""" + @property @abstractmethod - def defaults(self) -> Mapping[str, Any]: + def defaults(self) -> Mapping[str, str]: """All the DEFAULT values (not interpolated).""" @@ -215,6 +228,7 @@ def defaults(self) -> Mapping[str, Any]: class _IniValues(_ConfigValues): parser: configparser.ConfigParser + @property def sections(self) -> List[str]: return self.parser.sections() @@ -230,10 +244,196 @@ def get_value(self, section: str, option: str) -> Optional[str]: def options(self, section: str) -> List[str]: return self.parser.options(section) + @property def defaults(self) -> Mapping[str, str]: return self.parser.defaults() +_TomlPrimitve = Union[bool, int, float, str] +_TomlValue = Union[_TomlPrimitve, List[_TomlPrimitve]] + + +@dataclass(frozen=True) +class _TomlValues(_ConfigValues): + values: Dict[str, Any] + + @staticmethod + def _is_an_option(option_value: Union[_TomlValue, Dict]) -> bool: + """Determine if the value is actually an option belonging to that section. + + A value that looks like an option might actually be a subscope, e.g. the option value + `java` belonging to the section `cache` could actually be the section `cache.java`, rather + than the option `--cache-java`. + + We must also handle the special syntax of `my_list_option.add` and `my_list_option.remove`. + """ + if isinstance(option_value, dict): + return "add" in option_value or "remove" in option_value + return True + + @staticmethod + def _section_explicitly_defined(section_values: Dict) -> bool: + """Determine if the section is truly a defined section, meaning that the user explicitly wrote + the section in their config file. + + For example, the user may have explicitly defined `cache.java` but never defined `cache`. Due + to TOML's representation of the config as a nested dictionary, naively, it would appear that + `cache` was defined even though the user never explicitly added it to their config. + """ + at_least_one_option_defined = any( + _TomlValues._is_an_option(section_value) for section_value in section_values.values() + ) + # We also check if the section was explicitly defined but has no options. We can be confident + # that this is not a parent scope (e.g. `cache` when `cache.java` is really what was defined) + # because the parent scope would store its child scope in its values, so the values would not + # be empty. + blank_section = len(section_values.values()) == 0 + return at_least_one_option_defined or blank_section + + def _find_section_values(self, section: str) -> Optional[Dict]: + """Find the values for a section, if any. + + For example, if the config file was `{'GLOBAL': {'foo': 1}}`, this function would return + `{'foo': 1}` given `section='GLOBAL'`. + """ + def recurse(mapping: Dict, *, remaining_sections: List[str]) -> Optional[Dict]: + if not remaining_sections: + return None + current_section = remaining_sections[0] + if current_section not in mapping: + return None + section_values = mapping[current_section] + if len(remaining_sections) > 1: + return recurse(section_values, remaining_sections=remaining_sections[1:]) + if not self._section_explicitly_defined(section_values): + return None + return cast(Dict, section_values) + + return recurse(mapping=self.values, remaining_sections=section.split(".")) + + def _possibly_interpolate_value(self, raw_value: str) -> str: + """For any values with %(foo)s, substitute it with the corresponding default val.""" + def format_str(value: str) -> str: + # Because dictionaries use the symbols `{}`, we must proactively escape the symbols so that + # .format() does not try to improperly interpolate. + escaped_str = value.replace("{", "{{").replace("}", "}}") + new_style_format_str = re.sub( + pattern=r"%\((?P[a-zA-Z_0-9]*)\)s", + repl=r"{\g}", + string=escaped_str, + ) + return new_style_format_str.format(**self.defaults) + + def recursively_format_str(value: str) -> str: + # It's possible to interpolate with a value that itself has an interpolation. We must fully + # evaluate all expressions for parity with configparser. + if not re.search(r"%\([a-zA-Z_0-9]*\)s", value): + return value + return recursively_format_str(value=format_str(value)) + + return recursively_format_str(raw_value) + + def _stringify_val( + self, raw_value: _TomlValue, *, interpolate: bool = True, list_prefix: Optional[str] = None, + ) -> str: + """For parity with configparser, we convert all values back to strings, which allows us to + avoid upstream changes to files like parser.py. + + This is clunky. If we drop INI support, we should remove this and use native values.""" + if isinstance(raw_value, str): + return self._possibly_interpolate_value(raw_value) if interpolate else raw_value + + if isinstance(raw_value, list): + def stringify_list_member(member: _TomlPrimitve) -> str: + if not isinstance(member, str): + return str(member) + interpolated_member = self._possibly_interpolate_value(member) if interpolate else member + return f'"{interpolated_member}"' + + list_members = ", ".join(stringify_list_member(member) for member in raw_value) + return f"{list_prefix or ''}[{list_members}]" + + return str(raw_value) + + @property + def sections(self) -> List[str]: + sections: List[str] = [] + + def recurse(mapping: Dict, *, parent_section: Optional[str] = None) -> None: + for section, section_values in mapping.items(): + if not isinstance(section_values, dict): + continue + # We filter out "DEFAULT" and also check for the special `my_list_option.add` and + # `my_list_option.remove` syntax. + if section == "DEFAULT" or "add" in section_values or "remove" in section_values: + continue + section_name = section if not parent_section else f"{parent_section}.{section}" + if self._section_explicitly_defined(section_values): + sections.append(section_name) + recurse(section_values, parent_section=section_name) + + recurse(self.values) + return sections + + def has_section(self, section: str) -> bool: + return self._find_section_values(section) is not None + + def has_option(self, section: str, option: str) -> bool: + try: + self.get_value(section, option) + except (configparser.NoSectionError, configparser.NoOptionError): + return False + else: + return True + + def get_value(self, section: str, option: str) -> Optional[str]: + section_values = self._find_section_values(section) + if section_values is None: + raise configparser.NoSectionError(section) + if option in self.defaults: + return self._stringify_val(self.defaults[option]) + if option not in section_values: + raise configparser.NoOptionError(option, section) + option_value = section_values[option] + # Handle the special `my_list_option.add` and `my_list_option.remove` syntax. + if isinstance(option_value, dict): + has_add = "add" in option_value + has_remove = "remove" in option_value + if not has_add and not has_remove: + raise configparser.NoOptionError(option, section) + add_val = ( + self._stringify_val(option_value["add"], list_prefix="+") if has_add else None + ) + remove_val = ( + self._stringify_val(option_value["remove"], list_prefix="-") if has_remove else None + ) + if has_add and has_remove: + return f"{add_val},{remove_val}" + if has_add: + return add_val + return remove_val + return self._stringify_val(option_value) + + def options(self, section: str) -> List[str]: + section_values = self._find_section_values(section) + if section_values is None: + raise configparser.NoSectionError(section) + options = [ + option + for option, option_value in section_values.items() + if self._is_an_option(option_value) + ] + options.extend(self.defaults.keys()) + return options + + @property + def defaults(self) -> Mapping[str, str]: + return { + option: self._stringify_val(option_val, interpolate=False) + for option, option_val in self.values["DEFAULT"].items() + } + + @dataclass(frozen=True) class _EmptyConfig(Config): """A dummy config with no data at all.""" @@ -274,7 +474,7 @@ def sources(self) -> List[str]: return [self.config_path] def sections(self) -> List[str]: - return self.values.sections() + return self.values.sections def has_section(self, section: str) -> bool: return self.values.has_section(section) diff --git a/src/python/pants/option/config_test.py b/src/python/pants/option/config_test.py index 1a54e4d370d..5b08400323d 100644 --- a/src/python/pants/option/config_test.py +++ b/src/python/pants/option/config_test.py @@ -1,92 +1,152 @@ # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +import configparser +from abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import Enum from textwrap import dedent from typing import Dict +import pytest from twitter.common.collections import OrderedSet from pants.option.config import Config from pants.testutil.test_base import TestBase from pants.util.contextutil import temporary_file +from pants.util.enums import match -class ConfigTest(TestBase): +class ConfigFormat(Enum): + ini = "ini" + toml = "toml" - def _setup_config(self, config1_content: str, config2_content: str, *, suffix: str) -> Config: - with temporary_file(binary_mode=False, suffix=suffix) as config1, \ - temporary_file(binary_mode=False, suffix=suffix) as config2: - config1.write(config1_content) - config1.close() - config2.write(config2_content) - config2.close() - parsed_config = Config.load( - config_paths=[config1.name, config2.name], seed_values={"buildroot": self.build_root} - ) - assert [config1.name, config2.name] == parsed_config.sources() - return parsed_config - def setUp(self) -> None: - ini1_content = dedent( - """ - [DEFAULT] - name: foo - answer: 42 - scale: 1.2 - path: /a/b/%(answer)s - embed: %(path)s::foo - disclaimer: - Let it be known - that. - - [a] - list: [1, 2, 3, %(answer)s] - list2: +[7, 8, 9] - - [b] - preempt: True - - [b.nested] - dict: { - 'a': 1, - 'b': %(answer)s, - 'c': ['%(answer)s', '%(answer)s'], - } - - [b.nested.nested-again] - movie: inception - """ - ) - ini2_content = dedent( - """ - [a] - fast: True - - [b] - preempt: False - - [c.child] - no_values_in_parent: True - - [defined_section] - """ - ) - self.config = self._setup_config(ini1_content, ini2_content, suffix=".ini") - self.default_seed_values = Config._determine_seed_values( - seed_values={"buildroot": self.build_root}, +# MyPy doesn't like mixing dataclasses with ABC, which is indeed weird to do, but it works. +@dataclass(frozen=True) # type: ignore[misc] +class ConfigFile(ABC): + format: ConfigFormat + + @property + def suffix(self) -> str: + return match(self.format, {ConfigFormat.ini: ".ini", ConfigFormat.toml: ".toml"}) + + @property + @abstractmethod + def content(self) -> str: + pass + + @property + @abstractmethod + def default_values(self) -> Dict: + pass + + @property + @abstractmethod + def expected_options(self) -> Dict: + pass + + +class File1(ConfigFile): + + @property + def content(self) -> str: + return match( + self.format, + { + ConfigFormat.ini: dedent( + """ + [DEFAULT] + name: foo + answer: 42 + scale: 1.2 + path: /a/b/%(answer)s + embed: %(path)s::foo + disclaimer: + Let it be known + that. + + [a] + list: [1, 2, 3, %(answer)s] + list2: +[7, 8, 9] + list3: -["x", "y", "z"] + + [b] + preempt: True + + [b.nested] + dict: { + 'a': 1, + 'b': %(answer)s, + 'c': ['%(answer)s', '%(answer)s'], + } + + [b.nested.nested-again] + movie: inception + """ + ), + ConfigFormat.toml: dedent( + """ + [DEFAULT] + name = "foo" + answer = 42 + scale = 1.2 + path = "/a/b/%(answer)s" + embed = "%(path)s::foo" + disclaimer = ''' + Let it be known + that.''' + + [a] + # TODO: once TOML releases its new version with support for heterogenous lists, we should be + # able to rewrite this to `[1, 2, 3, "%(answer)s"`. See + # https://github.com/toml-lang/toml/issues/665. + list = ["1", "2", "3", "%(answer)s"] + list2.add = [7, 8, 9] + list3.remove = ["x", "y", "z"] + + [b] + preempt = true + + [b.nested] + dict = ''' + { + "a": 1, + "b": "%(answer)s", + "c": ["%(answer)s", "%(answer)s"], + }''' + + [b.nested.nested-again] + movie = "inception" + """ + ), + } ) - self.default_file1_values = { + + @property + def default_values(self): + common_values = { "name": "foo", "answer": "42", "scale": "1.2", "path": "/a/b/42", "embed": "/a/b/42::foo", - "disclaimer": "\nLet it be known\nthat.", } - self.expected_file1_options = { + return match( + self.format, + { + ConfigFormat.ini: {**common_values, "disclaimer": "\nLet it be known\nthat."}, + ConfigFormat.toml: {**common_values, "disclaimer": "Let it be known\nthat."} + } + ) + + @property + def expected_options(self) -> Dict: + ini_values = { "a": { "list": "[1, 2, 3, 42]", "list2": "+[7, 8, 9]", + "list3": '-["x", "y", "z"]', }, "b": { "preempt": "True", @@ -98,7 +158,75 @@ def setUp(self) -> None: "movie": "inception", }, } - self.expected_file2_options: Dict[str, Dict[str, str]] = { + return match( + self.format, + { + ConfigFormat.ini: ini_values, + ConfigFormat.toml: { + **ini_values, + "a": { + **ini_values["a"], "list": '["1", "2", "3", "42"]', + }, + "b.nested": { + "dict": '{\n "a": 1,\n "b": "42",\n "c": ["42", "42"],\n}' + }, + } + } + ) + + +class File2(ConfigFile): + + @property + def content(self) -> str: + return match( + self.format, + { + ConfigFormat.ini: dedent( + """ + [a] + fast: True + + [b] + preempt: False + + [c.child] + no_values_in_parent: True + + [d] + list: +[0, 1],-[8, 9] + + [defined_section] + """ + ), + ConfigFormat.toml: dedent( + """ + [a] + fast = true + + [b] + preempt = false + + [c.child] + no_values_in_parent = true + + [d] + list.add = [0, 1] + list.remove = [8, 9] + + [defined_section] + """ + ), + } + ) + + @property + def default_values(self) -> Dict: + return {} + + @property + def expected_options(self) -> Dict: + return { "a": { "fast": "True", }, @@ -108,19 +236,49 @@ def setUp(self) -> None: "c.child": { "no_values_in_parent": "True", }, + "d": { + "list": "+[0, 1],-[8, 9]", + }, "defined_section": {}, } - self.expected_combined_values: Dict[str, Dict[str, str]] = { - **self.expected_file1_options, - **self.expected_file2_options, + + +class ConfigBaseTest(TestBase): + __test__ = False + + # Subclasses should define these + file1 = File1(ConfigFormat.ini) + file2 = File2(ConfigFormat.ini) + + def _setup_config(self) -> Config: + with temporary_file(binary_mode=False, suffix=self.file1.suffix) as config1, \ + temporary_file(binary_mode=False, suffix=self.file2.suffix) as config2: + config1.write(self.file1.content) + config1.close() + config2.write(self.file2.content) + config2.close() + parsed_config = Config.load( + config_paths=[config1.name, config2.name], seed_values={"buildroot": self.build_root} + ) + assert [config1.name, config2.name] == parsed_config.sources() + return parsed_config + + def setUp(self) -> None: + self.config = self._setup_config() + self.default_seed_values = Config._determine_seed_values( + seed_values={"buildroot": self.build_root}, + ) + self.expected_combined_values = { + **self.file1.expected_options, + **self.file2.expected_options, "a": { - **self.expected_file2_options["a"], **self.expected_file1_options["a"], + **self.file2.expected_options["a"], **self.file1.expected_options["a"], }, } def test_sections(self) -> None: expected_sections = list( - OrderedSet([*self.expected_file2_options.keys(), *self.expected_file1_options.keys()]) + OrderedSet([*self.file2.expected_options.keys(), *self.file1.expected_options.keys()]) ) assert self.config.sections() == expected_sections for section in expected_sections: @@ -131,22 +289,22 @@ def test_sections(self) -> None: def test_has_option(self) -> None: # Check has all DEFAULT values - for default_option in (*self.default_seed_values.keys(), *self.default_file1_values.keys()): + for default_option in (*self.default_seed_values.keys(), *self.file1.default_values.keys()): assert self.config.has_option(section="DEFAULT", option=default_option) is True # Check every explicitly defined section has its options + the seed defaults for section, options in self.expected_combined_values.items(): for option in (*options, *self.default_seed_values): assert self.config.has_option(section=section, option=option) is True # Check every section for file1 also has file1's DEFAULT values - for section in self.expected_file1_options: - for option in self.default_file1_values: + for section in self.file1.expected_options: + for option in self.file1.default_values: assert self.config.has_option(section=section, option=option) is True # Check that file1's DEFAULT values don't apply to sections only defined in file2 - sections_only_in_file2 = set(self.expected_file2_options.keys()) - set( - self.expected_file1_options.keys() + sections_only_in_file2 = set(self.file2.expected_options.keys()) - set( + self.file1.expected_options.keys() ) for section in sections_only_in_file2: - for option in self.default_file1_values: + for option in self.file1.default_values: assert self.config.has_option(section=section, option=option) is False # Check that non-existent options are False nonexistent_options = { @@ -156,44 +314,56 @@ def test_has_option(self) -> None: } for section, option in nonexistent_options.items(): assert self.config.has_option(section=section, option=option) is False + # Check that sections aren't misclassified as options + nested_sections = { + "b": "nested", + "b.nested": "nested-again", + "c": "child", + } + for parent_section, child_section in nested_sections.items(): + assert self.config.has_option(section=parent_section, option=child_section) is False def test_list_all_options(self) -> None: # This is used in `options_bootstrapper.py` to validate that every option is recognized. file1_config = self.config.configs()[1] file2_config = self.config.configs()[0] - for section, options in self.expected_file1_options.items(): + for section, options in self.file1.expected_options.items(): assert file1_config.values.options(section=section) == [ - *options.keys(), *self.default_seed_values.keys(), *self.default_file1_values.keys(), + *options.keys(), *self.default_seed_values.keys(), *self.file1.default_values.keys(), ] - for section, options in self.expected_file2_options.items(): + for section, options in self.file2.expected_options.items(): assert file2_config.values.options(section=section) == [ *options.keys(), *self.default_seed_values.keys()] + # Check non-existent section + for config in file1_config, file2_config: + with pytest.raises(configparser.NoSectionError): + config.values.options("fake") def test_default_values(self) -> None: # This is used in `options_bootstrapper.py` to ignore default values when validating options. file1_config = self.config.configs()[1] file2_config = self.config.configs()[0] # NB: string interpolation should only happen when calling _ConfigValues.get_value(). The - # values for _ConfigValues.defaults() are not yet interpolated. + # values for _ConfigValues.defaults are not yet interpolated. default_file1_values_unexpanded = { - **self.default_file1_values, "path": "/a/b/%(answer)s", "embed": "%(path)s::foo", + **self.file1.default_values, "path": "/a/b/%(answer)s", "embed": "%(path)s::foo", } - assert file1_config.values.defaults() == { + assert file1_config.values.defaults == { **self.default_seed_values, **default_file1_values_unexpanded, } - assert file2_config.values.defaults() == self.default_seed_values + assert file2_config.values.defaults == self.default_seed_values def test_get(self) -> None: # Check the DEFAULT section - for option, value in {**self.default_seed_values, **self.default_file1_values}.items(): + for option, value in {**self.default_seed_values, **self.file1.default_values}.items(): assert self.config.get(section="DEFAULT", option=option) == value # Check the combined values, including that each section has the default seed values for section, section_values in self.expected_combined_values.items(): for option, value in {**section_values, **self.default_seed_values}.items(): assert self.config.get(section=section, option=option) == value # Check that each section from file1 also has file1's default values - for section in self.expected_file1_options: - for option, value in self.default_file1_values.items(): + for section in self.file1.expected_options: + for option, value in self.file1.default_values.items(): assert self.config.get(section=section, option=option) == value def check_defaults(default: str) -> None: @@ -210,3 +380,27 @@ def test_empty(self) -> None: assert config.sources() == [] assert config.has_section("DEFAULT") is False assert config.has_option(section="DEFAULT", option="name") is False + + +class ConfigIniTest(ConfigBaseTest): + __test__ = True + file1 = File1(ConfigFormat.ini) + file2 = File2(ConfigFormat.ini) + + +class ConfigTomlTest(ConfigBaseTest): + __test__ = True + file1 = File1(ConfigFormat.toml) + file2 = File2(ConfigFormat.toml) + + +class ConfigIniWithTomlTest(ConfigBaseTest): + __test__ = True + file1 = File1(ConfigFormat.ini) + file2 = File2(ConfigFormat.toml) + + +class ConfigTomlWithIniTest(ConfigBaseTest): + __test__ = True + file1 = File1(ConfigFormat.toml) + file2 = File2(ConfigFormat.ini) diff --git a/src/python/pants/option/options_bootstrapper.py b/src/python/pants/option/options_bootstrapper.py index 2372aca3375..b6eddc22df8 100644 --- a/src/python/pants/option/options_bootstrapper.py +++ b/src/python/pants/option/options_bootstrapper.py @@ -7,6 +7,7 @@ import stat import sys from dataclasses import dataclass +from pathlib import Path from typing import Dict, Iterable, List, Mapping, Optional, Sequence, Set, Tuple, Type from pants.base.build_environment import get_default_pants_config_file @@ -53,8 +54,9 @@ def get_config_file_paths(env, args) -> List[str]: evars = ['PANTS_GLOBAL_PANTS_CONFIG_FILES', 'PANTS_PANTS_CONFIG_FILES', 'PANTS_CONFIG_FILES'] path_list_values = [] - if os.path.isfile(get_default_pants_config_file()): - path_list_values.append(ListValueComponent.create(get_default_pants_config_file())) + default = get_default_pants_config_file() + if Path(default).is_file(): + path_list_values.append(ListValueComponent.create(default)) for var in evars: if var in env: path_list_values.append(ListValueComponent.create(env[var])) @@ -222,7 +224,7 @@ def verify_configs_against_options(self, options: Options) -> None: else: # All the options specified under [`section`] in `config` excluding bootstrap defaults. all_options_under_scope = ( - set(config.values.options(section)) - set(config.values.defaults()) + set(config.values.options(section)) - set(config.values.defaults) ) for option in all_options_under_scope: if option not in valid_options_under_scope: