Skip to content

Add recursive file and directory settings discovery.#63

Open
SerGeRybakov wants to merge 2 commits into
jag-k:mainfrom
SerGeRybakov:feature/recursive-export-from-dir
Open

Add recursive file and directory settings discovery.#63
SerGeRybakov wants to merge 2 commits into
jag-k:mainfrom
SerGeRybakov:feature/recursive-export-from-dir

Conversation

@SerGeRybakov

@SerGeRybakov SerGeRybakov commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

This PR extends settings discovery so the project can load BaseSettings classes not only from Python import strings, but also from Python files and directories scanned recursively.

It also fixes a long-standing ambiguity in generated outputs when multiple settings classes share the same class name but come from different files. Generated artifacts now include explicit source information for colliding top-level settings, so .env, Markdown, simple text, and TOML outputs remain understandable and distinguishable.

What changed

  • Added support for resolving settings sources from:

    • module
    • module:attribute
    • ./path/to/file.py
    • ./path/to/directory (recursive *.py scan)
  • Applied the new resolution logic consistently to:

    • global default_settings
    • per-generator settings
    • per-generator extend_settings
    • positional CLI arguments
  • Made relative filesystem paths resolve from project_dir

  • Added deduplication so the same settings class is not processed multiple times when discovered through multiple source forms

  • Preserved partial-success behavior for generator-level mixed source lists:

    • invalid sources emit warnings
    • valid sources are still processed
  • Improved generated output for colliding top-level settings names by adding explicit source information to duplicate names

  • Updated docs and config field descriptions to document file and directory based discovery

Collision handling

Before this change, if two different files defined the same top-level settings class name, generated outputs could contain visually ambiguous duplicate sections such as two AppSettings blocks with no indication of origin.

Now, when duplicate top-level names are detected in the same generation run, those settings are rendered with an explicit source suffix, for example:

  • AppSettings [./config/api_settings.py:AppSettings]
  • AppSettings [./config/worker_settings.py:AppSettings]

This applies across all built-in generators so that:

  • .env group headers are distinguishable
  • Markdown sections are distinguishable
  • simple text sections are distinguishable
  • TOML header comments are distinguishable

Unique settings names are left unchanged.

Tests

The test suite was expanded with real filesystem-based scenarios created under tmp_path, without mocking the core import/discovery logic.

Coverage includes:

  • valid legacy and new source formats
  • invalid module/file/directory combinations
  • mixed old and new source lists
  • recursive directory discovery
  • deduplication across multiple source forms
  • CLI integration with real temp files
  • generator-level settings / extend_settings behavior
  • output collision scenarios across all built-in generators:
    • identical implementation in different files
    • same implementation shape with different defaults
    • different implementation shape under the same class name

Summary by CodeRabbit

Release Notes

  • New Features

    • Settings can now be imported from Python files and directories with recursive discovery, in addition to module paths.
    • Enhanced settings source tracking and automatic disambiguation when multiple settings share the same name.
  • Documentation

    • Expanded configuration examples and documentation across README and configuration files to showcase new file and directory import capabilities.
  • Improvements

    • Enhanced CLI error handling and messaging for improved user feedback.
  • Chores

    • Updated pre-commit hook dependencies for ruff, mypy, and uv tools.

Allow CLI and generator configs to load BaseSettings from Python files and directories, disambiguate duplicated (same-named) settings from different sources in generated outputs.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@SerGeRybakov has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 48 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 7 minutes and 48 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ada2b84-83e8-4abe-b2eb-76ad2d4a4440

📥 Commits

Reviewing files that changed from the base of the PR and between 67f9578 and 865b7f7.

📒 Files selected for processing (5)
  • pydantic_settings_export/models.py
  • pydantic_settings_export/utils.py
  • tests/conftest.py
  • tests/test_cli.py
  • tests/test_exporter.py
📝 Walkthrough

Walkthrough

The pull request extends pydantic-settings-export to support importing settings from Python file paths and recursively discovering settings from directories, in addition to existing module/class path imports. It refactors import logic into a unified utility, adds source tracking to settings models, implements disambiguation for duplicate names, and updates CLI/exporter/generator flows accordingly with comprehensive test coverage.

Changes

Cohort / File(s) Summary
Core Import Logic
pydantic_settings_export/utils.py
Substantially refactored import_settings_from_string to detect and resolve filesystem paths (files and directories), support recursive discovery using importlib.util.spec_from_file_location, and handle project_dir context. Added new public import_settings_from_strings helper for batch importing with error-continuation and deduplication via _settings_identity function.
Settings Model Metadata
pydantic_settings_export/models.py
Added source field to SettingsInfoModel to track settings origin (filepath or module). Introduced settings_source() helper that resolves filesystem paths and computes relative display paths. Extended SettingsInfoModel.from_settings_model to populate the new source attribute and restructure instance/class detection logic.
CLI and Exporter Integration
pydantic_settings_export/cli.py, pydantic_settings_export/exporter.py
Updated CLI to resolve default settings via import_settings_from_strings instead of per-entry iteration; added centralized error handling in _load_settings_or_exit. Refactored Exporter._import_all to use the new plural import API with continue_on_error=True, removing manual error handling and consolidating import behavior.
Generator Disambiguation
pydantic_settings_export/generators/abstract.py, pydantic_settings_export/generators/markdown.py
Added _disambiguate_settings_infos() logic in AbstractGenerator that rewrites duplicate setting names to include [Source] suffix for clarity. MarkdownGenerator.generate now pre-processes inputs through disambiguation before generation.
Documentation and Configuration Examples
README.md, examples/Configuration.md, examples/InjectedConfiguration.md, examples/SimpleConfiguration.md, examples/config.example.toml, examples/pyproject.example.toml, examples/.env.example
Updated all documentation and configuration examples to reflect support for file paths (./app/settings.py) and directory paths (./app/settings) alongside existing module paths, with expanded descriptions of import formats and example lists.
Project Configuration
.pre-commit-config.yaml, pydantic_settings_export/settings.py
Bumped pre-commit hook versions (ruff-pre-commit, mirrors-mypy, uv-pre-commit). Expanded PSESettings.default_settings field documentation to document file and directory path support with updated examples.
Test Infrastructure
tests/conftest.py, tests/test_cli.py, tests/test_exporter.py, tests/test_utils.py
Added two new fixture dataclasses (SettingsSourcesProject, CollidingSettingsProject) that create temporary project structures for realistic import testing. Converted CLI tests from mocked to end-to-end integration tests. Added comprehensive tests for file/directory discovery, error handling, deduplication, and partial failure resilience.

Sequence Diagram

sequenceDiagram
    participant Config as Config/CLI
    participant ImportUtils as Import Utils
    participant Discovery as Path/Module<br/>Discovery
    participant Dedup as Deduplication
    participant Generator as Generator
    
    Config->>ImportUtils: import_settings_from_strings(paths)
    loop For each path
        ImportUtils->>Discovery: Resolve path (module/file/dir)
        alt Module path
            Discovery-->>ImportUtils: Import module<br/>Extract BaseSettings
        else File path
            Discovery->>Discovery: Load module from file<br/>via spec_from_file_location
            Discovery-->>ImportUtils: Extract BaseSettings
        else Directory path
            Discovery->>Discovery: Recursively traverse<br/>and import .py files
            Discovery-->>ImportUtils: Collect all BaseSettings
        end
    end
    
    ImportUtils->>Dedup: Deduplicate by source<br/>and qualified name
    Dedup-->>ImportUtils: Deduplicated settings list
    ImportUtils-->>Config: Return settings list
    
    Config->>Generator: Disambiguate duplicate<br/>names with [Source]
    Generator-->>Config: Generate output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

feature request, documentation, Complexity / S

Suggested reviewers

  • jag-k
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add recursive file and directory settings discovery' directly and accurately summarizes the main feature introduced: extending settings discovery to support recursive file and directory scanning, which is the core functionality of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 92.54% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added documentation Improvements or additions to documentation Complexity / S Complex to implement feature request labels Apr 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pydantic_settings_export/models.py`:
- Around line 288-307: The issue is that settings_source assumes the argument is
a BaseSettings class/instance and treats plain BaseModel instances as classes,
causing attribute access errors when getfile fails; update settings_source to
detect both BaseSettings and BaseModel instances (e.g., import and check
isinstance(settings, (BaseSettings, BaseModel))) and always set settings_class =
settings.__class__ for instances before calling getfile, so the fallback and
__qualname__ access use the class object; also mirror this change in
from_settings_model and any other callers referenced (the recursion points like
from_settings_model) so nested BaseModel instances are normalized to their class
before deriving the source.

In `@pydantic_settings_export/utils.py`:
- Around line 284-289: importlib.import_module(module_name) may return a cached
module from sys.modules that points to a different file; before returning the
imported module (inside the try in the block that uses _make_module_name and
_prepend_sys_path), validate that the module's __file__ matches the expected
path for the target settings file (use the path variable passed in) and only
return the module if it matches; if it does not match, treat it as a failed
import (fall through to the existing synthetic import fallback) or explicitly
raise so the fallback logic that creates a synthetic module executes. Ensure
this check is performed where importlib.import_module(module_name) is called and
reference the module object returned to inspect module.__file__.

In `@tests/conftest.py`:
- Around line 28-66: The fixture path properties (module_file_source,
discovered_dir_source, standalone_file_source, problematic_dir_source,
no_settings_file_source, broken_syntax_source, broken_import_source,
text_file_source) currently use f"...{Path.relative_to(... )!s}" which yields
OS-specific separators on Windows; create a small helper that calls
Path.relative_to(self.root).as_posix() and use it in each property (and for the
colliding_settings_project.sources entries) so all returned fixture source
strings are normalized to POSIX-style forward slashes for TOML embedding.

In `@tests/test_cli.py`:
- Around line 311-315: Normalize Windows backslashes before embedding paths into
the TOML test configs by calling as_posix() on Path objects used when writing
the config; specifically update the config_file.write_text calls that
interpolate output_file (and any other Path variables used in those TOML
strings) to use output_file.as_posix() so backslashes aren’t emitted and TOML
basic strings aren’t mis-parsed.

In `@tests/test_exporter.py`:
- Around line 41-51: The test test_exporter_init_default_generators currently
suppresses all warnings during Exporter() construction which hides
generator-init failures; change the warnings block to capture warnings (use
warnings.catch_warnings(record=True) and warnings.simplefilter("always")) when
calling Exporter(), then assert that the captured warnings list is empty (or
explicitly fail with the warning contents) before continuing to inspect
exporter.generators; reference Exporter() construction and
AbstractGenerator.ALL_GENERATORS so any generator initialization warnings cause
the test to fail rather than be silently ignored.
- Around line 366-393: The test
test_generator_settings_mixed_old_and_new_sources_are_deduplicated currently
only counts the exact string "AppSettings\n" which can miss duplicate sections
that are disambiguated like "AppSettings [source]"; update the assertion that
checks for duplicates (after calling Exporter.run_all via exporter.run_all()) to
count all heading lines that begin with "AppSettings" (regardless of trailing
disambiguation) — for example by matching lines that start with "AppSettings" or
using a startswith/regex on content lines — so the test fails if more than one
AppSettings section (with or without suffix) is present; keep references to
SimpleGenerator/SimpleSettings usage and the output_file content variable when
making the new assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: be3e3a88-9f4e-498d-847b-fa39f77c5363

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa5292 and 67f9578.

📒 Files selected for processing (19)
  • .pre-commit-config.yaml
  • README.md
  • examples/.env.example
  • examples/Configuration.md
  • examples/InjectedConfiguration.md
  • examples/SimpleConfiguration.md
  • examples/config.example.toml
  • examples/pyproject.example.toml
  • pydantic_settings_export/cli.py
  • pydantic_settings_export/exporter.py
  • pydantic_settings_export/generators/abstract.py
  • pydantic_settings_export/generators/markdown.py
  • pydantic_settings_export/models.py
  • pydantic_settings_export/settings.py
  • pydantic_settings_export/utils.py
  • tests/conftest.py
  • tests/test_cli.py
  • tests/test_exporter.py
  • tests/test_utils.py

Comment thread pydantic_settings_export/models.py Outdated
Comment thread pydantic_settings_export/utils.py
Comment thread tests/conftest.py Outdated
Comment thread tests/test_cli.py
Comment thread tests/test_exporter.py
Comment thread tests/test_exporter.py
Handle nested BaseModel instances when deriving settings sources, validate file imports against the expected module path, and normalize test/config path serialization so relative sources remain cross-platform.
@jag-k

jag-k commented Apr 23, 2026

Copy link
Copy Markdown
Owner

I think it is better to do two things:

  • Split the deduplication into another PR
  • Replace file/dir logic with more universal glob-patterns

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Complexity / S Complex to implement documentation Improvements or additions to documentation feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants