Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions platformio/project/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,21 @@
)
@click.option("--lint", is_flag=True)
@click.option("--json-output", is_flag=True)
def project_config_cmd(project_dir, lint, json_output):
@click.option(
"--show-source",
is_flag=True,
help="Show the config file where each option is defined.",
)
def project_config_cmd(project_dir, lint, json_output, show_source):
if not is_platformio_project(project_dir):
raise NotPlatformIOProjectError(project_dir)
with fs.cd(project_dir):
if lint:
return lint_configuration(json_output)
return print_configuration(json_output)
return print_configuration(json_output, show_source)


def print_configuration(json_output=False):
def print_configuration(json_output=False, show_source=False):
config = ProjectConfig.get_instance()
if json_output:
return click.echo(config.to_json())
Expand All @@ -51,15 +56,15 @@ def print_configuration(json_output=False):
for section, options in config.as_tuple():
click.secho(section, fg="cyan")
click.echo("-" * len(section))
click.echo(
tabulate(
[
(name, "=", "\n".join(value) if isinstance(value, list) else value)
for name, value in options
],
tablefmt="plain",
)
)
rows = []
for name, value in options:
display_value = "\n".join(value) if isinstance(value, list) else value
row = [name, "=", display_value]
if show_source:
source = config.get_source(section, name)
row.append("; from " + os.path.basename(source) if source else "")
rows.append(row)
click.echo(tabulate(rows, tablefmt="plain"))
click.echo()
return None

Expand Down
26 changes: 26 additions & 0 deletions platformio/project/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def __init__(self, path=None, parse_extra=True, expand_interpolations=True):
self.expand_interpolations = expand_interpolations
self.warnings = []
self._parsed = []
self._source_map = {}
self._parser = configparser.ConfigParser(inline_comment_prefixes=("#", ";"))
if path and os.path.isfile(path):
self.read(path, parse_extra)
Expand All @@ -112,6 +113,8 @@ def read(self, path, parse_extra=True):
except configparser.Error as exc:
raise exception.InvalidProjectConfError(path, str(exc)) from exc

self._update_source_map(path)

if not parse_extra:
return

Expand All @@ -122,6 +125,29 @@ def read(self, path, parse_extra=True):
for item in glob.glob(pattern, recursive=True):
self.read(item)

def _update_source_map(self, path):
"""Record which file defines each (section, option) pair.

Independently parses the file to discover its sections and options.
Later files override earlier ones, matching configparser's merge
semantics.
"""
file_parser = configparser.ConfigParser(inline_comment_prefixes=("#", ";"))
try:
file_parser.read(path, "utf-8")
except configparser.Error:
return
for section in file_parser.sections():
for option in file_parser.options(section):
self._source_map[(section, option)] = path

def get_source(self, section, option):
"""Return the file path where the given option was last defined.

Returns None if the option is not found in any parsed file.
"""
return self._source_map.get((section, option))

def _maintain_renamed_options(self):
renamed_options = {}
for option in ProjectOptions.values():
Expand Down
130 changes: 86 additions & 44 deletions tests/project/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,15 +438,13 @@ def test_items(config):

def test_update_and_save(tmpdir_factory):
tmpdir = tmpdir_factory.mktemp("project")
tmpdir.join("platformio.ini").write(
"""
tmpdir.join("platformio.ini").write("""
[platformio]
extra_configs = a.ini, b.ini

[env:myenv]
board = myboard
"""
)
""")
config = ProjectConfig(tmpdir.join("platformio.ini").strpath)
assert config.envs() == ["myenv"]
assert config.as_tuple()[0][1][0][1] == ["a.ini", "b.ini"]
Expand Down Expand Up @@ -487,15 +485,13 @@ def test_update_and_save(tmpdir_factory):

def test_update_and_clear(tmpdir_factory):
tmpdir = tmpdir_factory.mktemp("project")
tmpdir.join("platformio.ini").write(
"""
tmpdir.join("platformio.ini").write("""
[platformio]
extra_configs = a.ini, b.ini

[env:myenv]
board = myboard
"""
)
""")
config = ProjectConfig(tmpdir.join("platformio.ini").strpath)
assert config.sections() == ["platformio", "env:myenv"]
config.update([["mysection", [("opt1", "value1"), ("opt2", "value2")]]], clear=True)
Expand Down Expand Up @@ -588,12 +584,10 @@ def test_win_core_root_dir(tmpdir_factory):

# Override in config
tmpdir = tmpdir_factory.mktemp("project")
tmpdir.join("platformio.ini").write(
"""
tmpdir.join("platformio.ini").write("""
[platformio]
core_dir = ~/.pio
"""
)
""")
config = ProjectConfig(tmpdir.join("platformio.ini").strpath)
assert config.get("platformio", "core_dir") != win_core_root_dir
assert config.get("platformio", "core_dir") == os.path.realpath(
Expand All @@ -608,17 +602,15 @@ def test_win_core_root_dir(tmpdir_factory):

def test_this(tmp_path: Path):
project_conf = tmp_path / "platformio.ini"
project_conf.write_text(
"""
project_conf.write_text("""
[common]
board = uno

[env:myenv]
extends = common
build_flags = -D${this.__env__}
custom_option = ${this.board}
"""
)
""")
config = ProjectConfig(str(project_conf))
assert config.get("env:myenv", "custom_option") == "uno"
assert config.get("env:myenv", "build_flags") == ["-Dmyenv"]
Expand All @@ -628,30 +620,25 @@ def test_project_name(tmp_path: Path):
project_dir = tmp_path / "my-project-name"
project_dir.mkdir()
project_conf = project_dir / "platformio.ini"
project_conf.write_text(
"""
project_conf.write_text("""
[env:myenv]
"""
)
""")
with fs.cd(str(project_dir)):
config = ProjectConfig(str(project_conf))
assert config.get("platformio", "name") == "my-project-name"

# custom name
project_conf.write_text(
"""
project_conf.write_text("""
[platformio]
name = custom-project-name
"""
)
""")
config = ProjectConfig(str(project_conf))
assert config.get("platformio", "name") == "custom-project-name"


def test_nested_interpolation(tmp_path: Path):
project_conf = tmp_path / "platformio.ini"
project_conf.write_text(
"""
project_conf.write_text("""
[platformio]
build_dir = /tmp/pio-$PROJECT_HASH
data_dir = $PROJECT_DIR/assets
Expand All @@ -669,8 +656,7 @@ def test_nested_interpolation(tmp_path: Path):
16000000L
${UPLOAD_PORT and "-p "+UPLOAD_PORT}
${platformio.build_dir}/${this.__env__}/firmware.elf
"""
)
""")
config = ProjectConfig(str(project_conf))
assert config.get("platformio", "data_dir").endswith(
os.path.join("$PROJECT_DIR", "assets")
Expand All @@ -688,8 +674,7 @@ def test_nested_interpolation(tmp_path: Path):

def test_extends_order(tmp_path: Path):
project_conf = tmp_path / "platformio.ini"
project_conf.write_text(
"""
project_conf.write_text("""
[a]
board = test

Expand All @@ -701,33 +686,28 @@ def test_extends_order(tmp_path: Path):

[env:na_ti-ve13]
extends = a, b, c
"""
)
""")
config = ProjectConfig(str(project_conf))
assert config.get("env:na_ti-ve13", "upload_tool") == "three"


def test_invalid_env_names(tmp_path: Path):
project_conf = tmp_path / "platformio.ini"
project_conf.write_text(
"""
project_conf.write_text("""
[env:app:1]
"""
)
""")
config = ProjectConfig(str(project_conf))
with pytest.raises(InvalidEnvNameError, match=r".*Invalid environment name 'app:1"):
config.validate()


def test_linting_errors(tmp_path: Path):
project_conf = tmp_path / "platformio.ini"
project_conf.write_text(
"""
project_conf.write_text("""
[env:app1]
lib_use = 1
broken_line
"""
)
""")
result = ProjectConfig.lint(str(project_conf))
assert not result["warnings"]
assert result["errors"] and len(result["errors"]) == 1
Expand All @@ -738,18 +718,80 @@ def test_linting_errors(tmp_path: Path):

def test_linting_warnings(tmp_path: Path):
project_conf = tmp_path / "platformio.ini"
project_conf.write_text(
"""
project_conf.write_text("""
[platformio]
build_dir = /tmp/pio-$PROJECT_HASH

[env:app1]
lib_use = 1
test_testing_command = /usr/bin/flash-tool -p $UPLOAD_PORT -b $UPLOAD_SPEED
"""
)
""")
result = ProjectConfig.lint(str(project_conf))
assert not result["errors"]
assert result["warnings"] and len(result["warnings"]) == 2
assert "deprecated" in result["warnings"][0]
assert "Invalid variable declaration" in result["warnings"][1]


def test_get_source_single_file(tmp_path: Path):
project_conf = tmp_path / "platformio.ini"
project_conf.write_text("""
[platformio]
src_dir = src

[env:myenv]
board = esp32
build_flags = -DFOO
""")
config = ProjectConfig(str(project_conf))
assert config.get_source("platformio", "src_dir") == str(project_conf)
assert config.get_source("env:myenv", "board") == str(project_conf)
assert config.get_source("env:myenv", "build_flags") == str(project_conf)


def test_get_source_extra_configs(config):
# The module-scoped `config` fixture has three files:
# - platformio.ini (BASE_CONFIG)
# - extra_envs.ini (EXTRA_ENVS_CONFIG)
# - extra_debug.ini (EXTRA_DEBUG_CONFIG)

# Options only in platformio.ini
assert os.path.basename(config.get_source("platformio", "env_default")) == (
"platformio.ini"
)
assert os.path.basename(config.get_source("env:base", "build_flags")) == (
"platformio.ini"
)

# Options only in extra_envs.ini
assert os.path.basename(config.get_source("env:extra_1", "build_flags")) == (
"extra_envs.ini"
)
assert os.path.basename(config.get_source("env:extra_2", "upload_port")) == (
"extra_envs.ini"
)

# Options overridden by extra_debug.ini (last file wins)
# custom.debug_flags is defined in platformio.ini and overridden in extra_debug.ini
assert os.path.basename(config.get_source("custom", "debug_flags")) == (
"extra_debug.ini"
)
# env:extra_2.build_flags is in extra_envs.ini and overridden in extra_debug.ini
assert os.path.basename(config.get_source("env:extra_2", "build_flags")) == (
"extra_debug.ini"
)


def test_get_source_unknown_option(config):
assert config.get_source("custom", "nonexistent_option") is None
assert config.get_source("nonexistent_section", "option") is None


def test_get_source_no_extra(tmp_path: Path):
project_conf = tmp_path / "platformio.ini"
project_conf.write_text("""
[env:myenv]
board = esp32
""")
config = ProjectConfig(str(project_conf), parse_extra=False)
assert config.get_source("env:myenv", "board") == str(project_conf)