Skip to content

Commit 71b5898

Browse files
committed
fix: multiple paths passed to robocop check/format command resolving to the same config
Fixed a bug where config was not correctly resolved if sources were directly passed to robocop. First found configuration was used instead of searching for config for all passed sources.
1 parent 0e7f04e commit 71b5898

File tree

3 files changed

+69
-30
lines changed

3 files changed

+69
-30
lines changed

src/robocop/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "7.3.0" # x-release-please-version
1+
__version__ = "7.2.0" # x-release-please-version

src/robocop/config_manager.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,7 @@ def __init__(
120120
self.force_exclude = force_exclude
121121
self.skip_gitignore = skip_gitignore
122122
self.gitignore_resolver = GitIgnoreResolver()
123-
self.overridden_config = (
124-
config is not None
125-
) # TODO: what if both cli and --config? should take --config then apply cli
123+
self.overridden_config = config is not None
126124
self.root = root or Path.cwd()
127125
self.sources = sources
128126
self.default_config: Config = self.get_default_config(config)
@@ -160,9 +158,6 @@ def get_default_config(self, config_path: Path | None) -> Config:
160158
sources = [Path(path).resolve() for path in self.sources] if self.sources else [Path.cwd()]
161159
directories = files.get_common_parent_dirs(sources)
162160
config = self.find_config_in_dirs(directories, default=None)
163-
if not config:
164-
config = Config()
165-
self.cached_configs.update(dict.fromkeys(directories, config))
166161
else:
167162
config = Config()
168163
config.overwrite_from_config(self.overwrite_config)
@@ -174,47 +169,66 @@ def is_git_project_root(self, path: Path) -> bool:
174169
return False
175170
return (path / ".git").is_dir()
176171

177-
def find_closest_config(self, source: Path, default: Config | None) -> Config:
178-
"""Look in the directory and its parents for the closest valid configuration file."""
179-
return self.find_config_in_dirs(source.parents, default)
172+
def find_config_in_directory(self, directory: Path) -> Config | None:
173+
"""
174+
Search for a configuration file in the specified directory.
175+
176+
This method iterates through predefined configuration filenames and attempts
177+
to locate and load the first matching configuration file found in the given
178+
directory. Only configuration files with valid Robocop entry are taken into account.
179+
180+
Args:
181+
directory: The directory path to search for configuration files.
182+
183+
Returns:
184+
A Config object if a configuration file is found and successfully
185+
loaded, otherwise None.
186+
187+
"""
188+
for config_filename in CONFIG_NAMES:
189+
if (config_path := (directory / config_filename)).is_file():
190+
configuration = files.read_toml_config(config_path)
191+
if configuration:
192+
config = Config.from_toml(configuration, config_path)
193+
config.overwrite_from_config(self.overwrite_config) # TODO those two lines together
194+
if config.verbose:
195+
print(f"Loaded {config_path} configuration file.")
196+
return config
197+
return None
180198

181199
def find_config_in_dirs(self, directories: list[Path], default: Config | None) -> Config:
182200
seen = [] # if we find config, mark all visited directories with resolved config
201+
found = default
183202
for check_dir in directories:
184203
if check_dir in self.cached_configs:
185-
return self.cached_configs[check_dir]
204+
found = self.cached_configs[check_dir]
205+
break
186206
seen.append(check_dir)
187-
for config_filename in CONFIG_NAMES:
188-
if (config_path := (check_dir / config_filename)).is_file():
189-
configuration = files.read_toml_config(config_path)
190-
if configuration:
191-
config = Config.from_toml(configuration, config_path)
192-
config.overwrite_from_config(self.overwrite_config) # TODO those two lines together
193-
self.cached_configs.update(dict.fromkeys(seen, config))
194-
if config.verbose:
195-
print(f"Loaded {config_path} configuration file.")
196-
return config
207+
if (directory_config := self.find_config_in_directory(check_dir)) is not None:
208+
found = directory_config
209+
break
197210
if self.is_git_project_root(check_dir):
198211
break
199212

200-
if default:
201-
self.cached_configs.update(dict.fromkeys(seen, default))
202-
return default
213+
if found is None:
214+
found = Config()
215+
self.cached_configs.update(dict.fromkeys(seen, found))
216+
return found
203217

204218
def get_config_for_source_file(self, source_file: Path) -> Config:
205219
"""
206220
Find the closest config to the source file or directory.
207221
208-
If it was loaded before it will be returned from the cache. Otherwise, we will load it and save it to cache
209-
first.
222+
If it was loaded before, it will be returned from the cache. Otherwise, we will
223+
load it and save it to the cache first.
210224
211225
Args:
212226
source_file: Path to Robot Framework source file or directory.
213227
214228
"""
215229
if self.overridden_config or self.ignore_file_config:
216230
return self.default_config
217-
return self.find_closest_config(source_file, self.default_config)
231+
return self.find_config_in_dirs(source_file.parents, self.default_config)
218232

219233
def resolve_paths(
220234
self,
@@ -233,7 +247,6 @@ def resolve_paths(
233247
ignore_file_filters: force robocop to parse file even if it's excluded in the configuration
234248
235249
"""
236-
source_gitignore = None
237250
config = None
238251
for source in sources:
239252
source_not_resolved = Path(source)
@@ -244,8 +257,7 @@ def resolve_paths(
244257
if source_not_resolved.is_symlink(): # i.e. dangling symlink
245258
continue
246259
raise exceptions.FatalError(f"File '{source}' does not exist")
247-
if config is None: # first file in the directory
248-
config = self.get_config_for_source_file(source)
260+
config = self.get_config_for_source_file(source)
249261
if not ignore_file_filters:
250262
if config.file_filters.path_excluded(source_not_resolved):
251263
continue

tests/config/test_config_manager.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,33 @@ def test_relative_paths_from_config_option(self, test_data):
303303
== (relative_parent / "custom_formatters").resolve()
304304
)
305305

306+
def test_multiple_sources_and_configs(self, tmp_path):
307+
"""Multiple source paths passed to the entry command, with different configuration files."""
308+
# Arrange - create two test files with two closest configurations
309+
config_file_1 = tmp_path / "pyproject.toml"
310+
config_file_1.write_text("[tool.robocop]\nverbose = true\n", encoding="utf-8")
311+
first_config = Config()
312+
first_config.verbose = True
313+
first_config.config_source = str(config_file_1)
314+
test_file_1 = tmp_path / "test.robot"
315+
test_file_1.write_text("*** Test Cases ***\nTest\n Log Hello\n", encoding="utf-8")
316+
317+
config_file_2 = tmp_path / "subdir" / "pyproject.toml"
318+
config_file_2.parent.mkdir(parents=True, exist_ok=True)
319+
config_file_2.write_text("[tool.robocop]\nverbose = false\n", encoding="utf-8")
320+
second_config = Config()
321+
second_config.verbose = False
322+
second_config.config_source = str(config_file_2)
323+
test_file_2 = tmp_path / "subdir" / "test.robot"
324+
test_file_2.write_text("*** Test Cases ***\nTest\n Log Hello\n", encoding="utf-8")
325+
expected_results = {test_file_1: first_config, test_file_2: second_config}
326+
327+
# Act
328+
actual_results = get_sources_and_configs(tmp_path, sources=[test_file_1, test_file_2])
329+
330+
# Assert
331+
assert actual_results == expected_results
332+
306333
def test_fail_on_deprecated_config_options(self, test_data, capsys):
307334
"""Unknown or deprecated options in the configuration file should raise an error."""
308335
config_path = test_data / "old_config" / "pyproject.toml"

0 commit comments

Comments
 (0)