Full Code of lincolnloop/goodconf for AI

main 235cc2882bfb cached
22 files
48.7 KB
12.9k tokens
70 symbols
1 requests
Download .txt
Repository: lincolnloop/goodconf
Branch: main
Commit: 235cc2882bfb
Files: 22
Total size: 48.7 KB

Directory structure:
gitextract_x6obglxq/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── build.yml
│       └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .yamllint.yml
├── CHANGES.rst
├── LICENSE
├── README.rst
├── goodconf/
│   ├── __init__.py
│   ├── contrib/
│   │   ├── __init__.py
│   │   ├── argparse.py
│   │   └── django.py
│   └── py.typed
├── pyproject.toml
└── tests/
    ├── __init__.py
    ├── test_django.py
    ├── test_file_helpers.py
    ├── test_files.py
    ├── test_goodconf.py
    ├── test_initial.py
    └── utils.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# https://editorconfig.org/
root = true

[*]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
charset = utf-8
max_line_length = 88

[*.py]
indent_size = 4

[*.md]
indent_size = 4

[Makefile]
indent_style = tab


================================================
FILE: .github/workflows/build.yml
================================================
---
name: Publish to PyPI
"on":
  push:
    branches: [main, test-publish]
    tags: ["*"]
  pull_request:

jobs:
  build:
    name: Build distribution
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Install hatch
        run: pip install hatch
      - name: Build a binary wheel and a source tarball
        run: hatch build
      - name: Store the distribution packages
        uses: actions/upload-artifact@v4
        with:
          name: python-package-distributions
          path: dist/

  pypi-publish:
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
    needs: [build]
    name: Upload release to PyPI
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/goodconf
    permissions:
      id-token: write
    steps:
      - name: Download all the dists
        uses: actions/download-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
      - name: Publish distribution to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1

  github-release:
    name: >-
      Sign the Python 🐍 distribution 📦 with Sigstore
      and upload them to GitHub Release
    needs:
      - pypi-publish
    runs-on: ubuntu-latest

    permissions:
      contents: write # IMPORTANT: mandatory for making GitHub Releases
      id-token: write # IMPORTANT: mandatory for sigstore

    steps:
      - name: Download all the dists
        uses: actions/download-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
      - name: Sign the dists with Sigstore
        uses: sigstore/gh-action-sigstore-python@v3.0.0
        with:
          inputs: >-
            ./dist/*.tar.gz
            ./dist/*.whl
      - name: Create GitHub Release
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: >-
          gh release create
          '${{ github.ref_name }}'
          --repo '${{ github.repository }}'
          --generate-notes
      - name: Upload artifact signatures to GitHub Release
        env:
          GITHUB_TOKEN: ${{ github.token }}
        # Upload to GitHub Release using the `gh` CLI.
        # `dist/` contains the built packages, and the
        # sigstore-produced signatures and certificates.
        run: >-
          gh release upload
          '${{ github.ref_name }}' dist/**
          --repo '${{ github.repository }}'


================================================
FILE: .github/workflows/test.yml
================================================
---
name: test

"on":
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read # to fetch code (actions/checkout)

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
    env:
      PYTHON: ${{ matrix.python }}
    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        uses: astral-sh/setup-uv@v5

      - name: Set up Python ${{ matrix.python }}
        run: uv python install ${{ matrix.python }}

      - name: Install dependencies
        run: uv sync --group dev --python ${{ matrix.python }}

      - name: Test
        run: uv run pytest --cov-report=xml --cov-report=term --junitxml=junit.xml

      - name: Upload coverage to Codecov
        if: ${{ (success() || failure()) && matrix.python == '3.14' }}
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: true

      - name: Upload test results to Codecov
        if: ${{ (success() || failure()) && matrix.python == '3.14' }}
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          report_type: test_results
          fail_ci_if_error: true


================================================
FILE: .gitignore
================================================
*.egg-info
.coverage
.pytest_cache
/build
/dist
/goodconf/_version.py
__pycache__
env
htmlcov
junit.xml
uv.lock
venv


================================================
FILE: .pre-commit-config.yaml
================================================
---
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-yaml
      - id: end-of-file-fixer
      - id: mixed-line-ending
      - id: trailing-whitespace
      - id: file-contents-sorter
        files: (.dockerignore|.gitignore)

  - repo: https://github.com/adrienverge/yamllint.git
    rev: v1.38.0
    hooks:
      - id: yamllint
        args: [--strict]

  - repo: https://github.com/python-jsonschema/check-jsonschema
    rev: 0.37.0
    hooks:
      - id: check-github-workflows

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.5
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
    rev: v2.16.0
    hooks:
      - id: pretty-format-toml
        args: [--autofix, --trailing-commas]
        types: [toml]
        files: \.toml

  - repo: https://github.com/ninoseki/uv-sort
    rev: v0.7.0
    hooks:
      - id: uv-sort

  - repo: local
    hooks:
      - id: prettier
        name: prettier
        language: node
        entry: prettier
        args: [--write, --ignore-unknown]
        additional_dependencies: [prettier@3]
        exclude_types: [python]
        pass_filenames: true

  - repo: local
    hooks:
      - id: mypy
        name: mypy
        language: python
        entry: mypy goodconf
        types: [python]
        require_serial: true
        pass_filenames: false
        additional_dependencies: [mypy]


================================================
FILE: .yamllint.yml
================================================
---
extends: default
rules:
  comments:
    min-spaces-from-content: 1
  line-length:
    max: 88


================================================
FILE: CHANGES.rst
================================================
==========
Change Log
==========

7.0.0 (3 March 2026)
========================

- **Backwards Incompatible Release**

  - Removed official support for Python 3.9
  - Added official support for Python 3.13 and 3.14
  - Updated pydantic-settings requirement to >=2.13 (was >=2.4)

6.0.0 (8 October 2024)
========================

- **Backwards Incompatible Release**

  - Removed the ``_config_file`` attribute from ``GoodConf``.
    If you previously set this attribute, you are no longer be able to do so.
- Support reading TOML files via ``tomllib`` on Python 3.11+
- Update Markdown generation, so that output matches v4 output

5.0.0 (13 August 2024)
========================

- **Backwards Incompatible Release**

  - Removed official support for Python 3.8
  - upgraded to pydantic2

  To upgrade from goodconf v4 to goodconf v5:

  - If subclassing ``GoodConf``, replace uses of ``class Config`` with ``model_config``.

    For example goodconf v4 code like this:

    .. code:: python

        from goodconf import GoodConf

        class AppConfig(GoodConf):
            "Configuration for My App"
            DATABASE_URL: PostgresDsn = "postgres://localhost:5432/mydb"

            class Config:
                default_files = ["/etc/myproject/myproject.yaml", "myproject.yaml"]

        config = AppConfig()

    should be replaced in goodconf v5 with:

    .. code:: python

        from goodconf import GoodConf

        class AppConfig(GoodConf):
            "Configuration for My App"
            DATABASE_URL: PostgresDsn = "postgres://localhost:5432/mydb"

            model_config = {"default_files": ["/etc/myproject/myproject.yaml", "myproject.yaml"]}

        config = AppConfig()

4.0.3 (13 August 2024)
========================

- Release from GitHub Actions

4.0.2 (11 February 2024)
========================

- Another markdown output fix
- Fix for markdown generation generation on Python 3.8 & 3.9

4.0.1 (10 February 2024)
========================

- Fix trailing whitespace in markdown output

4.0.0 (10 February 2024)
========================

- Removed errant print statement
- Removed official support for Python 3.7
- Added support for Python 3.12

3.1.0 (10 February 2024)
========================

- Fixed type display in Markdown generation
- Changed markdown output format (trailing spaces were problematic).

3.0.1 (30 June 2023)
====================

- pin to pydantic < 2 due to breaking changes in 2.0

3.0.0 (17 January 2023)
==================

- TOML files are now supported as configuration source
- Python 3.11 and 3.10 are now officially supported
- Python 3.6 is no longer officially supported
- Requires Pydantic 1.7+
- Variables can now be set during class initialization


2.0.1 (15 June 2021)
====================

- Change to newer syntax for safe loading yaml


2.0.0 (13 May 2021)
===================

- **Backwards Incompatible Release**
    Internals replaced with `pydantic <https://pypi.org/project/pydantic/>`_. Users can either pin to ``1.0.0`` or update their code as follows:

    - Replace ``goodconf.Value`` with ``goodconf.Field``.
    - Replace ``help`` keyword argument with ``description`` in ``Field`` (previously ``Value``).
    - Remove ``cast_as`` keyword argument from ``Field`` (previously ``Value``). Standard Python type annotations are now used.
    - Move ``file_env_var`` and ``default_files`` keyword arguments used in class initialization to a sub-class named ``Config``

    Given a version ``1`` class that looks like this:

    .. code:: python

        from goodconf import GoodConf, Value

        class AppConfig(GoodConf):
            "Configuration for My App"
            DEBUG = Value(default=False, help="Toggle debugging.")
            MAX_REQUESTS = Value(cast_as=int)

        config = AppConfig(default_files=["config.yml"])

    A class updated for version `2` would be:

    .. code:: python

        from goodconf import GoodConf, Field

        class AppConfig(GoodConf):
            "Configuration for My App"
            DEBUG: bool = Field(default=False, description="Toggle debugging.")
            MAX_REQUESTS: int

            class Config:
                default_files=["config.yml"]

        config = AppConfig()

2.0b3 (15 April 2021)
=====================

- Environment variables take precedence over configuration files in the event of a conflict

2.0b2 (12 March 2021)
=====================

- Use null value for initial if allowed
- Store the config file parsed as ``GoodConf.Config._config_file``


2.0b1 (11 March 2021)
=====================

- Backwards Incompatible: Migrated backend to ``pydantic``.

  - ``Value`` is replaced by the `Field function <https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation>`__.
  - ``help`` keyword arg is now ``description``
  - ``GoodConf`` is now backed by `BaseSettings <https://pydantic-docs.helpmanual.io/usage/settings/>`__
    Instead of passing keyword args when instantiating the class, they are now defined on a ``Config`` class on the object



1.0.0 (18 July 2018)
====================

- Allow overriding of values in the generate_* methods
- Python 3.7 supported


0.9.1 (10 April 2018)
=====================

- Explicit ``load`` method
- ``django_manage`` method helper on ``GoodConf``
- Fixed a few minor bugs


0.9.0 (8 April 2018)
====================

- Use a declarative class to define GoodConf's values.

- Change description to a docstring of the class.

- Remove the redundant ``required`` argument from ``Values``. To make
  an value optional, give it a default.

- Changed implicit loading to happen during instanciation rather than first
  access. Instanciate with ``load=False`` to avoid loading config initially.

0.8.3 (28 Mar 2018)
===================

- Implicitly load config if not loaded by first access.

0.8.2 (28 Mar 2018)
===================

- ``-c`` is used by Django's ``collectstatic``. Using ``-C`` instead.

0.8.1 (28 Mar 2018)
===================

- Adds ``goodconf.contrib.argparse`` to add a config argument to an existing
  parser.

0.8.0 (27 Mar 2018)
===================

- Major refactor from ``file-or-env`` to ``goodconf``

0.6.1 (16 Mar 2018)
================

- Fixed packaging issue.

0.6.0 (16 Mar 2018)
================

- Fixes bug in stack traversal to find calling file.


0.5.1 (15 March 2018)
==================

- Initial release


================================================
FILE: LICENSE
================================================
Copyright (c) 2018 Lincoln Loop

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.rst
================================================
Goodconf
========

.. image:: https://github.com/lincolnloop/goodconf/actions/workflows/test.yml/badge.svg?branch=main&event=push
    :target: https://github.com/lincolnloop/goodconf/actions/workflows/test.yml?query=branch%3Amain+event%3Apush

.. image:: https://results.pre-commit.ci/badge/github/lincolnloop/goodconf/main.svg
    :target: https://results.pre-commit.ci/latest/github/lincolnloop/goodconf/main
    :alt: pre-commit.ci status

.. image:: https://img.shields.io/codecov/c/github/lincolnloop/goodconf.svg
    :target: https://codecov.io/gh/lincolnloop/goodconf

.. image:: https://img.shields.io/pypi/v/goodconf.svg
    :target: https://pypi.python.org/pypi/goodconf

.. image:: https://img.shields.io/pypi/pyversions/goodconf.svg
    :target: https://pypi.python.org/pypi/goodconf

A thin wrapper over `Pydantic's settings management <https://pydantic-docs.helpmanual.io/usage/settings/>`__.
Allows you to define configuration variables and load them from environment or JSON/YAML/TOML
file. Also generates initial configuration files and documentation for your
defined configuration.


Installation
------------

``pip install goodconf`` or ``pip install goodconf[yaml]`` /
``pip install goodconf[toml]`` if parsing/generating YAML/TOML
files is required. When running on Python 3.11+ the ``[toml]``
extra is only required for generating TOML files as parsing
is supported natively.


Quick Start
-----------

Let's use configurable Django settings as an example.

First, create a ``conf.py`` file in your project's directory, next to
``settings.py``:

.. code:: python

    import base64
    import os

    from goodconf import GoodConf, Field
    from pydantic import PostgresDsn

    class AppConfig(GoodConf):
        "Configuration for My App"
        DEBUG: bool
        DATABASE_URL: PostgresDsn = "postgres://localhost:5432/mydb"
        SECRET_KEY: str = Field(
            initial=lambda: base64.b64encode(os.urandom(60)).decode(),
            description="Used for cryptographic signing. "
            "https://docs.djangoproject.com/en/2.0/ref/settings/#secret-key")

        model_config = {"default_files": ["/etc/myproject/myproject.yaml", "myproject.yaml"]}

    config = AppConfig()

Next, use the config in your ``settings.py`` file:

.. code:: python

    import dj_database_url
    from .conf import config

    config.load()

    DEBUG = config.DEBUG
    SECRET_KEY = config.SECRET_KEY
    DATABASES = {"default": dj_database_url.parse(config.DATABASE_URL)}

In your initial developer installation instructions, give some advice such as:

.. code:: shell

    python -c "import myproject; print(myproject.conf.config.generate_yaml(DEBUG=True))" > myproject.yaml

Better yet, make it a function and `entry point <https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation>`__ so you can install
your project and run something like ``generate-config > myproject.yaml``.

Usage
-----


``GoodConf``
^^^^^^^^^^^^

Your subclassed ``GoodConf`` object can include a ``model_config`` dictionary with the following
attributes:

``file_env_var``
  The name of an environment variable which can be used for
  the name of the configuration file to load.
``default_files``
  If no file is passed to the ``load`` method, try to load a
  configuration from these files in order.

It also has one method:

``load``
  Trigger the load method during instantiation. Defaults to False.

Use plain-text docstring for use as a header when generating a configuration
file.

Environment variables always take precedence over variables in the configuration files.

See Pydantic's docs for examples of loading:

* `Dotenv (.env) files <https://pydantic-docs.helpmanual.io/usage/settings/#dotenv-env-support>`_
* `Docker secrets <https://pydantic-docs.helpmanual.io/usage/settings/#secret-support>`_


Fields
^^^^^^

Declare configuration values by subclassing ``GoodConf`` and defining class
attributes which are standard Python type definitions or Pydantic ``FieldInfo``
instances generated by the ``Field`` function.

Goodconf can use one extra argument provided to the ``Field`` to define an function
which can generate an initial value for the field:

``initial``
  Callable to use for initial value when generating a config


Django Usage
------------

A helper is provided which monkey-patches Django's management commands to
accept a ``--config`` argument. Replace your ``manage.py`` with the following:

.. code:: python

    # Define your GoodConf in `myproject/conf.py`
    from myproject.conf import config

    if __name__ == '__main__':
        config.django_manage()


Why?
----

I took inspiration from `logan <https://github.com/dcramer/logan>`__ (used by
Sentry) and `derpconf <https://github.com/globocom/derpconf>`__ (used by
Thumbor). Both, however used Python files for configuration. I wanted a safer
format and one that was easier to serialize data into from a configuration
management system.

Environment Variables
^^^^^^^^^^^^^^^^^^^^^

I don't like working with environment variables. First, there are potential
security issues:

1. Accidental leaks via logging or error reporting services.
2. Child process inheritance (see `ImageTragick <https://imagetragick.com/>`__
   for an idea why this could be bad).

Second, in practice on deployment environments, environment variables end up
getting written to a number of files (cron, bash profile, service definitions,
web server config, etc.). Not only is it cumbersome, but also increases the
possibility of leaks via incorrect file permissions.

I prefer a single structured file which is explicitly read by the application.
I also want it to be easy to run my applications on services like Heroku
where environment variables are the preferred configuration method.

This module let's me do things the way I prefer in environments I control, but
still run them with environment variables on environments I don't control with
minimal fuss.


Contribute
----------

Install dependencies.

.. code:: shell

    uv sync


Run tests

.. code:: shell

    uv run pytest

Releases are done with GitHub Actions whenever a new tag is created. For more information,
see `<./.github/workflows/build.yml>`_


================================================
FILE: goodconf/__init__.py
================================================
"""
Transparently load variables from environment or JSON/YAML file.
"""

import errno
import json
import logging
import os
import sys
from functools import partial
from io import StringIO
from types import GenericAlias
from typing import Any, cast, get_args

from pydantic._internal._config import config_keys
from pydantic.fields import Field as PydanticField
from pydantic.fields import FieldInfo, PydanticUndefined
from pydantic.main import _object_setattr
from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
)

__all__ = ["Field", "GoodConf", "GoodConfConfigDict"]

log = logging.getLogger(__name__)


def Field(
    *args,
    initial=None,
    json_schema_extra=None,
    **kwargs,
):
    if initial:
        json_schema_extra = json_schema_extra or {}
        json_schema_extra["initial"] = initial

    return PydanticField(*args, json_schema_extra=json_schema_extra, **kwargs)


class GoodConfConfigDict(SettingsConfigDict):
    # configuration file to load
    file_env_var: str | None
    # if no file is given, try to load a configuration from these files in order
    default_files: list[str] | None


# Note: code from pydantic-settings/pydantic_settings/main.py:
# Extend `config_keys` by pydantic settings config keys to
# support setting config through class kwargs.
# Pydantic uses `config_keys` in `pydantic._internal._config.ConfigWrapper.for_model`
# to extract config keys from model kwargs, So, by adding pydantic settings keys to
# `config_keys`, they will be considered as valid config keys and will be collected
# by Pydantic.
config_keys |= set(GoodConfConfigDict.__annotations__.keys())


def _load_config(path: str) -> dict[str, Any]:
    """
    Given a file path, parse it based on its extension (YAML, TOML or JSON)
    and return the values as a Python dictionary. JSON is the default if an
    extension can't be determined.
    """
    __, ext = os.path.splitext(path)
    if ext in [".yaml", ".yml"]:
        import ruamel.yaml

        yaml = ruamel.yaml.YAML(typ="safe", pure=True)
        loader = yaml.load
    elif ext == ".toml":
        try:
            import tomllib

            def load(stream):
                return tomllib.loads(f.read())
        except ImportError:  # Fallback for Python < 3.11
            import tomlkit

            def load(stream):
                return tomlkit.load(f).unwrap()

        loader = load

    else:
        loader = json.load
    with open(path) as f:
        config = loader(f)
    return config or {}


def _find_file(filename: str, require: bool = True) -> str | None:
    if not os.path.exists(filename):
        if not require:
            return None
        raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), filename)
    return os.path.abspath(filename)


def _fieldinfo_to_str(field_info: FieldInfo) -> str:
    """
    Return the string representation of a pydantic.fields.FieldInfo.
    """
    if isinstance(field_info.annotation, type) and not isinstance(
        field_info.annotation, GenericAlias
    ):
        # For annotation like <class 'int'>, we use its name ("int").
        field_type = field_info.annotation.__name__
    elif str(field_info.annotation).startswith("typing."):
        # For annotation like typing.Literal['a', 'b'], we use
        # its string representation, but without "typing." ("Literal['a', 'b']").
        field_type = str(field_info.annotation)[len("typing.") :]
    else:
        # For annotation like list[str], we use its string
        # representation ("list[str]").
        field_type = field_info.annotation
    return field_type


def initial_for_field(name: str, field_info: FieldInfo) -> Any:
    try:
        json_schema_extra = field_info.json_schema_extra or {}
        if not callable(json_schema_extra["initial"]):
            raise ValueError(f"Initial value for `{name}` must be a callable.")
        return field_info.json_schema_extra["initial"]()
    except KeyError:
        if (
            field_info.default is not PydanticUndefined
            and field_info.default is not ...
        ):
            return field_info.default
        if field_info.default_factory is not None:
            return field_info.default_factory()
    if type(None) in get_args(field_info.annotation):
        return None
    return ""


class FileConfigSettingsSource(PydanticBaseSettingsSource):
    """
    Source class for loading values provided during settings class initialization.
    """

    def __init__(self, settings_cls: type[BaseSettings]):
        super().__init__(settings_cls)

    def get_field_value(
        self, field: FieldInfo, field_name: str
    ) -> tuple[Any, str, bool]:
        # Nothing to do here. Only implement the return statement to make mypy happy
        return None, "", False

    def __call__(self) -> dict[str, Any]:
        settings = cast("GoodConf", self.settings_cls)
        selected_config_file = None
        if cfg_file := self.current_state.get("_config_file"):
            selected_config_file = cfg_file
        elif (file_env_var := settings.model_config.get("file_env_var")) and (
            cfg_file := os.environ.get(file_env_var)
        ):
            selected_config_file = _find_file(cfg_file)
        else:
            for filename in settings.model_config.get("default_files") or []:
                selected_config_file = _find_file(filename, require=False)
                if selected_config_file:
                    break
        if selected_config_file:
            values = _load_config(selected_config_file)
            log.info("Loading config from %s", selected_config_file)
        else:
            values = {}
            log.info("No config file specified. Loading with environment variables.")
        return values

    def __repr__(self) -> str:
        return "FileConfigSettingsSource()"


class GoodConf(BaseSettings):
    def __init__(
        self, load: bool = False, config_file: str | None = None, **kwargs
    ) -> None:
        """
        :param load: load config file on instantiation [default: False].

        A docstring defined on the class should be a plain-text description
        used as a header when generating a configuration file.
        """
        if kwargs or load:  # Emulate Pydantic behavior, load immediately
            self._load(_init_config_file=config_file, **kwargs)
        elif config_file:
            _object_setattr(
                self, "_load", partial(self._load, _init_config_file=config_file)
            )

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        """Load environment variables before init"""
        return (
            init_settings,
            env_settings,
            dotenv_settings,
            FileConfigSettingsSource(settings_cls),
            file_secret_settings,
        )

    model_config = GoodConfConfigDict()

    @classmethod
    def _settings_build_values(
        cls,
        sources: tuple[PydanticBaseSettingsSource, ...],
        init_kwargs: dict[str, Any],
    ) -> dict[str, Any]:
        state = super()._settings_build_values(
            sources,
            init_kwargs,
        )
        state.pop("_config_file", None)
        return state

    def _load(
        self,
        _config_file: str | None = None,
        _init_config_file: str | None = None,
        **kwargs,
    ):
        if config_file := _config_file or _init_config_file:
            kwargs["_config_file"] = config_file
        super().__init__(**kwargs)

    def load(self, filename: str | None = None) -> None:
        self._load(_config_file=filename)

    @classmethod
    def get_initial(cls, **override) -> dict:
        return {
            k: override.get(k, initial_for_field(k, v))
            for k, v in cls.model_fields.items()
        }

    @classmethod
    def generate_yaml(cls, **override) -> str:
        """
        Dumps initial config in YAML
        """
        import ruamel.yaml

        yaml = ruamel.yaml.YAML()
        yaml.representer.add_representer(
            type(None),
            lambda self, d: self.represent_scalar("tag:yaml.org,2002:null", "~"),  # noqa: ARG005
        )
        yaml_str = StringIO()
        yaml.dump(cls.get_initial(**override), stream=yaml_str)
        yaml_str.seek(0)
        dict_from_yaml = yaml.load(yaml_str)
        if cls.__doc__:
            dict_from_yaml.yaml_set_start_comment("\n" + cls.__doc__ + "\n\n")
        for k in dict_from_yaml:
            if cls.model_fields[k].description:
                description = cast("str", cls.model_fields[k].description)
                dict_from_yaml.yaml_set_comment_before_after_key(
                    k, before="\n" + description
                )
        yaml_str = StringIO()
        yaml.dump(dict_from_yaml, yaml_str)
        yaml_str.seek(0)
        return yaml_str.read()

    @classmethod
    def generate_json(cls, **override) -> str:
        """
        Dumps initial config in JSON
        """
        return json.dumps(cls.get_initial(**override), indent=2)

    @classmethod
    def generate_toml(cls, **override) -> str:
        """
        Dumps initial config in TOML
        """
        import tomlkit
        from tomlkit.items import Item

        toml_str = tomlkit.dumps(cls.get_initial(**override))
        dict_from_toml = tomlkit.loads(toml_str)
        document = tomlkit.document()
        if cls.__doc__:
            document.add(tomlkit.comment(cls.__doc__))
        for k, v in dict_from_toml.unwrap().items():
            document.add(k, v)
            if cls.model_fields[k].description:
                description = cast("str", cls.model_fields[k].description)
                cast("Item", document[k]).comment(description)
        return tomlkit.dumps(document)

    @classmethod
    def generate_markdown(cls) -> str:
        """
        Documents values in markdown
        """
        lines = []
        if cls.__doc__:
            lines.extend([f"# {cls.__doc__}", ""])

        for k, field_info in cls.model_fields.items():
            lines.append(f"* **{k}**")
            if field_info.is_required():
                lines[-1] = f"{lines[-1]} _REQUIRED_"
            if field_info.description:
                lines.append(f"  * description: {field_info.description}")
            # We want to append a line with the field_info type, and sometimes
            # field_info.annotation looks the way we want, like 'list[str]', but
            # other times, it includes some extra text, like '<class 'bool'>'.
            # Therefore, we have some logic to make the type show up the way we want.
            field_type = _fieldinfo_to_str(field_info)
            lines.append(f"  * type: `{field_type}`")
            if field_info.default not in [None, PydanticUndefined]:
                lines.append(f"  * default: `{field_info.default}`")
        return "\n".join(lines)

    def django_manage(self, args: list[str] | None = None):
        args = args or sys.argv
        from .contrib.django import execute_from_command_line_with_config

        execute_from_command_line_with_config(self, args)


================================================
FILE: goodconf/contrib/__init__.py
================================================


================================================
FILE: goodconf/contrib/argparse.py
================================================
import argparse

from .. import GoodConf


def argparser_add_argument(parser: argparse.ArgumentParser, config: GoodConf):
    """Adds argument for config to existing argparser"""
    help = "Config file."
    cfg = config.model_config
    if cfg.get("file_env_var"):
        help += (
            "Can also be configured via the environment variable: "
            f"{cfg['file_env_var']}"
        )
    if cfg.get("default_files"):
        files_str = ", ".join(cfg["default_files"])
        help += f" Defaults to the first file that exists from [{files_str}]."
    parser.add_argument("-C", "--config", metavar="FILE", help=help)


================================================
FILE: goodconf/contrib/django.py
================================================
import argparse
from collections.abc import Generator
from contextlib import contextmanager

from .. import GoodConf
from .argparse import argparser_add_argument


@contextmanager
def load_config_from_cli(
    config: GoodConf, argv: list[str]
) -> Generator[list[str], None, None]:
    """Loads config, checking CLI arguments for a config file"""

    # Monkey patch Django's command parser
    from django.core.management.base import BaseCommand

    original_parser = BaseCommand.create_parser

    def patched_parser(self, prog_name, subcommand):
        parser = original_parser(self, prog_name, subcommand)
        argparser_add_argument(parser, config)
        return parser

    BaseCommand.create_parser = patched_parser

    try:
        parser = argparse.ArgumentParser(add_help=False)
        argparser_add_argument(parser, config)

        config_arg, default_args = parser.parse_known_args(argv)
        config.load(config_arg.config)
        yield default_args
    finally:
        # Put that create_parser back where it came from or so help me!
        BaseCommand.create_parser = original_parser


def execute_from_command_line_with_config(config: GoodConf, argv: list[str]):
    """Load's config then runs Django's execute_from_command_line"""
    with load_config_from_cli(config, argv) as args:
        from django.core.management import execute_from_command_line

        execute_from_command_line(args)


================================================
FILE: goodconf/py.typed
================================================


================================================
FILE: pyproject.toml
================================================
[build-system]
build-backend = "hatchling.build"
requires = ["hatchling", "hatch-vcs"]

[dependency-groups]
dev = [
  "django>=3.2.0",
  "mypy>=1.19.1",
  "pytest-cov==7.0.*",
  "pytest-mock==3.15.*",
  "pytest==9.0.*",
  "ruamel.yaml>=0.17.0",
  "tomlkit>=0.11.6",
]

[project]
authors = [
  {name = "Peter Baumgartner", email = "brett@python.org"},
]
classifiers = [
  "Development Status :: 5 - Production/Stable",
  "Intended Audience :: Developers",
  "License :: OSI Approved :: MIT License",
  "Operating System :: OS Independent",
  "Programming Language :: Python",
  "Programming Language :: Python :: 3.10",
  "Programming Language :: Python :: 3.11",
  "Programming Language :: Python :: 3.12",
  "Programming Language :: Python :: 3.13",
  "Programming Language :: Python :: 3.14",
]
dependencies = [
  "pydantic-settings>=2.13",
  "pydantic>=2.7",
]
description = "Load configuration variables from a file or environment"
dynamic = ["version"]
keywords = ["env", "config", "json", "yaml", "toml"]
license = {file = "LICENSE"}
name = "goodconf"
readme = "README.rst"
requires-python = ">=3.10"

[project.optional-dependencies]
toml = ["tomlkit>=0.11.6"]
yaml = ["ruamel.yaml>=0.17.0"]

[project.urls]
changelog = "https://github.com/lincolnloop/goodconf/blob/main/CHANGES.rst"
homepage = "https://github.com/lincolnloop/goodconf/"

[tool.coverage.report]
exclude_lines = [
  "if TYPE_CHECKING:",
  "pragma: no cover",
  "raise NotImplementedError",
]
skip_covered = true

[tool.coverage.run]
branch = true
disable_warnings = [
  "module-not-imported",
  "module-not-measured",
  "no-data-collected",
]
source = ["goodconf", "tests"]

[tool.hatch.build.hooks.vcs]
version-file = "goodconf/_version.py"

[tool.hatch.build.targets.sdist]
exclude = [
  "/.github",
]

[tool.hatch.version]
source = "vcs"

[tool.mypy]
ignore_errors = true

[tool.pytest.ini_options]
addopts = ["--cov", "--strict-markers"]

[tool.ruff.lint]
ignore = [
  # Permanently suppressed
  "A001",  # Variable shadowing builtin (intentional: `help` in argparse)
  "ANN401",  # Dynamically typed expressions (typing.Any) are disallowed in `**kwargs`
  "ARG001",  # Unused function argument
  "ARG002",  # Unused method argument
  "COM812",  # (ruff format) Checks for the absence of trailing commas
  "D",  # Missing or badly formatted docstrings
  "E501",  # Let the formatter handle long lines
  "FBT",  # Flake Boolean Trap (don't use arg=True in functions)
  "ISC001",  # (ruff format) Checks for implicitly concatenated strings on a single line
  "N802",  # Function name should be lowercase (Field is a public API name)
  "PLC0415",  # Import not at top level (intentional: lazy imports for optional deps)
  "RUF012",  # Mutable class attributes https://github.com/astral-sh/ruff/issues/5243
  # Temporary — see docs/plans/ruff-cleanup.md
  "ANN",  # Missing type annotations (annotation work deferred)
  "EM102",  # Exception message in f-string
  "PTH",  # Use pathlib instead of os.path (refactoring deferred)
  "TC002",  # Move import to TYPE_CHECKING block
  "TID252",  # Relative imports
  "TRY003",  # Long exception messages
  "TRY004",  # Prefer TypeError over AttributeError
]
select = ["ALL"]

[tool.ruff.lint.extend-per-file-ignores]
"test_*.py" = [
  "ANN001",  # Missing type annotation for function argument
  "ANN201",  # Missing return type annotation
  "PLR2004",  # Magic value used in comparison
  "S101",  # Use of `assert` detected
  "S105",  # Hardcoded password (test fixtures)
]


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/test_django.py
================================================
import sys

import pytest
from pydantic import ConfigDict

from goodconf import GoodConf

pytest.importorskip("django")


def test_mgmt_command(mocker, tmpdir):
    mocked_load_config = mocker.patch("goodconf._load_config")
    mocked_dj_execute = mocker.patch("django.core.management.execute_from_command_line")
    temp_config = tmpdir.join("config.yml")
    temp_config.write("")

    class G(GoodConf):
        model_config = ConfigDict()

    c = G()
    dj_args = ["manage.py", "diffsettings", "-v", "2"]
    c.django_manage([*dj_args, "-C", str(temp_config)])
    mocked_load_config.assert_called_once_with(str(temp_config))
    mocked_dj_execute.assert_called_once_with(dj_args)


def test_help(mocker, tmpdir, capsys):
    mocker.patch("sys.exit")
    mocked_load_config = mocker.patch("goodconf._load_config")
    temp_config = tmpdir.join("config.yml")
    temp_config.write("")

    class G(GoodConf):
        model_config = ConfigDict(
            file_env_var="MYAPP_CONF",
            default_files=["/etc/myapp.json"],
        )

    c = G()
    assert c.model_config.get("file_env_var") == "MYAPP_CONF"
    c.django_manage(
        [
            "manage.py",
            "diffsettings",
            "-C",
            str(temp_config),
            "--settings",
            __name__,
            "-h",
        ]
    )
    mocked_load_config.assert_called_once_with(str(temp_config))
    output = capsys.readouterr()
    if sys.version_info < (3, 13):
        assert "-C FILE, --config FILE" in output.out
    else:
        assert "-C, --config FILE" in output.out

    assert "MYAPP_CONF" in output.out
    assert "/etc/myapp.json" in output.out


# This doubles as a Django settings file for the tests
SECRET_KEY = "abc"


================================================
FILE: tests/test_file_helpers.py
================================================
import os
import sys

import pytest

from goodconf import _find_file, _load_config


def test_json(tmpdir):
    conf = tmpdir.join("conf.json")
    conf.write('{"a": "b", "c": 3}')
    assert _load_config(str(conf)) == {"a": "b", "c": 3}


def test_load_toml(tmpdir):
    if sys.version_info < (3, 11):
        pytest.importorskip("tomlkit")
    conf = tmpdir.join("conf.toml")
    conf.write('a = "b"\nc = 3')
    assert _load_config(str(conf)) == {"a": "b", "c": 3}


def test_load_empty_toml(tmpdir):
    if sys.version_info < (3, 11):
        pytest.importorskip("tomlkit")
    conf = tmpdir.join("conf.toml")
    conf.write("")
    assert _load_config(str(conf)) == {}


def test_yaml(tmpdir):
    pytest.importorskip("ruamel.yaml")
    conf = tmpdir.join("conf.yaml")
    conf.write("a: b\nc: 3")
    assert _load_config(str(conf)) == {"a": "b", "c": 3}


def test_load_empty_yaml(tmpdir):
    pytest.importorskip("ruamel.yaml")
    conf = tmpdir.join("conf.yaml")
    conf.write("")
    assert _load_config(str(conf)) == {}


def test_missing(tmpdir):
    conf = tmpdir.join("test.yml")
    assert _find_file(str(conf), require=False) is None


def test_missing_strict(tmpdir):
    conf = tmpdir.join("test.yml")
    with pytest.raises(FileNotFoundError):
        _find_file(str(conf))


def test_abspath(tmpdir):
    conf = tmpdir.join("test.yml")
    conf.write("")
    path = _find_file(str(conf))
    assert path == str(conf)


def test_relative(tmpdir):
    conf = tmpdir.join("test.yml")
    conf.write("")
    os.chdir(conf.dirname)
    assert _find_file("test.yml") == str(conf)


================================================
FILE: tests/test_files.py
================================================
import json

from goodconf import GoodConf

from .utils import env_var


def test_conf_env_var(mocker, tmpdir):
    mocked_load_config = mocker.patch("goodconf._load_config")
    path = tmpdir.join("myapp.json")
    path.write("")

    class G(GoodConf):
        model_config = {"file_env_var": "CONF"}

    with env_var("CONF", str(path)):
        g = G()
        g.load()
    mocked_load_config.assert_called_once_with(str(path))


def test_conflict(tmpdir):
    path = tmpdir.join("myapp.json")
    path.write(json.dumps({"A": 1, "B": 2}))

    class G(GoodConf):
        A: int
        B: int

        model_config = {"default_files": [path]}

    with env_var("A", "3"):
        g = G()
        g.load()
    assert g.A == 3
    assert g.B == 2


def test_all_env_vars(mocker):
    mocked_set_values = mocker.patch("goodconf.BaseSettings.__init__")
    mocked_load_config = mocker.patch("goodconf._load_config")

    class G(GoodConf):
        pass

    g = G()
    g.load()
    mocked_set_values.assert_called_once_with()
    mocked_load_config.assert_not_called()


def test_provided_file(mocker, tmpdir):
    mocked_load_config = mocker.patch("goodconf._load_config")
    path = tmpdir.join("myapp.json")
    path.write("")

    class G(GoodConf):
        pass

    g = G()
    g.load(str(path))
    mocked_load_config.assert_called_once_with(str(path))


def test_provided_file_from_init(mocker, tmpdir):
    mocked_load_config = mocker.patch("goodconf._load_config")
    path = tmpdir.join("myapp.json")
    path.write("")

    class G(GoodConf):
        pass

    g = G(config_file=str(path))
    g.load()
    mocked_load_config.assert_called_once_with(str(path))


def test_default_files(mocker, tmpdir):
    mocked_load_config = mocker.patch("goodconf._load_config")
    path = tmpdir.join("myapp.json")
    path.write("")
    bad_path = tmpdir.join("does-not-exist.json")

    class G(GoodConf):
        model_config = {"default_files": [str(bad_path), str(path)]}

    g = G()
    g.load()
    mocked_load_config.assert_called_once_with(str(path))


================================================
FILE: tests/test_goodconf.py
================================================
import json
import os
import re
from textwrap import dedent
from typing import Literal

import pytest
from pydantic import ValidationError
from pydantic.fields import FieldInfo

from goodconf import Field, FileConfigSettingsSource, GoodConf
from tests.utils import env_var


def test_initial():
    class TestConf(GoodConf):
        a: bool = Field(initial=lambda: True)
        b: bool = Field(default=False)

    initial = TestConf.get_initial()
    assert len(initial) == 2
    assert initial["a"] is True
    assert initial["b"] is False


def test_dump_json():
    class TestConf(GoodConf):
        a: bool = Field(initial=lambda: True)

    assert TestConf.generate_json() == '{\n  "a": true\n}'
    assert TestConf.generate_json(not_a_value=True) == '{\n  "a": true\n}'
    assert TestConf.generate_json(a=False) == '{\n  "a": false\n}'


def test_dump_toml():
    pytest.importorskip("tomlkit")

    class TestConf(GoodConf):
        a: bool = False
        b: str = "Happy"

    output = TestConf.generate_toml()
    assert "a = false" in output
    assert 'b = "Happy"' in output

    class TestConf(GoodConf):
        "Configuration for My App"

        a: str = Field(description="this is a")
        b: str

    output = TestConf.generate_toml()

    assert "# Configuration for My App\n" in output
    assert 'a = "" # this is a' in output
    assert 'b = ""' in output


def test_dump_yaml():
    pytest.importorskip("ruamel.yaml")

    class TestConf(GoodConf):
        "Configuration for My App"

        a: str = Field(description="this is a")
        b: str

    output = TestConf.generate_yaml()
    output = re.sub(r" +\n", "\n", output)
    assert "\n# Configuration for My App\n" in output
    assert (
        dedent(
            """\
        # this is a
        a: ''
        """
        )
        in output
    )
    assert "b: ''" in output

    output_override = TestConf.generate_yaml(b="yes")
    assert "a: ''" in output_override
    assert "b: yes" in output_override


def test_dump_yaml_no_docstring():
    pytest.importorskip("ruamel.yaml")

    class TestConf(GoodConf):
        a: str = Field(description="this is a")

    output = TestConf.generate_yaml()
    output = re.sub(r" +\n", "\n", output)
    assert output == dedent(
        """
        # this is a
        a: ''
        """
    )


def test_dump_yaml_none():
    pytest.importorskip("ruamel.yaml")

    class TestConf(GoodConf):
        a: str | None

    output = TestConf.generate_yaml()
    assert output.strip() == "a: ~"


def test_generate_markdown():
    help_ = "this is a"

    class TestConf(GoodConf):
        "Configuration for My App"

        a: int = Field(description=help_, default=None)
        b: int = Field(description=help_, default=5)
        c: str

    mkdn = TestConf.generate_markdown()
    # Not sure on final format, just do some basic smoke tests
    assert TestConf.__doc__ in mkdn
    assert help_ in mkdn


def test_generate_markdown_no_docstring():
    help_ = "this is a"

    class TestConf(GoodConf):
        a: int = Field(description=help_, default=5)
        b: str

    mkdn = TestConf.generate_markdown()
    # Not sure on final format, just do some basic smoke tests
    assert f"  * description: {help_}" in mkdn.splitlines()


def test_generate_markdown_default_false():
    class TestConf(GoodConf):
        a: bool = Field(default=False)

    lines = TestConf.generate_markdown().splitlines()
    assert "  * type: `bool`" in lines
    assert "  * default: `False`" in lines


def test_generate_markdown_types():
    class TestConf(GoodConf):
        a: Literal["a", "b"] = Field(default="a")
        b: list[str] = Field()
        c: None

    lines = TestConf.generate_markdown().splitlines()
    assert "  * type: `Literal['a', 'b']`" in lines
    assert "  * type: `list[str]`" in lines
    assert "default: `PydanticUndefined`" not in str(lines)


def test_generate_markdown_required():
    class TestConf(GoodConf):
        a: str

    lines = TestConf.generate_markdown().splitlines()
    assert "* **a** _REQUIRED_" in lines


def test_undefined():
    c = GoodConf()
    with pytest.raises(AttributeError):
        c.UNDEFINED  # noqa: B018


def test_required_missing():
    class TestConf(GoodConf):
        a: str = Field()

    c = TestConf()

    with pytest.raises(ValidationError):
        c.load()

    with pytest.raises(ValidationError):
        TestConf(load=True)


def test_default_values_are_used(monkeypatch):
    """
    Covers regression in: https://github.com/lincolnloop/goodconf/pull/51

    Requires more than one defined field to reproduce.
    """
    monkeypatch.delenv("a", raising=False)
    monkeypatch.setenv("b", "value_from_env")
    monkeypatch.delenv("c", raising=False)

    class TestConf(GoodConf):
        a: str = Field(default="default_for_a")
        b: str = Field(initial=lambda: "1234")
        c: str = Field("default_for_c")

    c = TestConf()
    c.load()

    assert c.a == "default_for_a"
    assert c.b == "value_from_env"
    assert c.c == "default_for_c"


def test_set_on_init():
    class TestConf(GoodConf):
        a: str = Field()

    val = "test"
    c = TestConf(a=val)
    assert c.a == val


def test_env_prefix():
    class TestConf(GoodConf):
        a: bool = False

        model_config = {"env_prefix": "PREFIX_"}

    with env_var("PREFIX_A", "True"):
        c = TestConf(load=True)

    assert c.a


def test_precedence(tmpdir):
    path = tmpdir.join("myapp.json")
    path.write(json.dumps({"init": "file", "env": "file", "file": "file"}))

    class TestConf(GoodConf, default_files=[path]):
        init: str = ""
        env: str = ""
        file: str = ""

    os.environ["INIT"] = "env"
    os.environ["ENV"] = "env"
    try:
        c = TestConf(init="init")
        assert c.init == "init"
        assert c.env == "env"
        assert c.file == "file"
    finally:
        del os.environ["INIT"]
        del os.environ["ENV"]


def test_fileconfigsettingssource_repr():
    class SettingsClass:
        model_config = {}

    fileconfigsettingssource = FileConfigSettingsSource(SettingsClass)

    assert repr(fileconfigsettingssource) == "FileConfigSettingsSource()"


def test_fileconfigsettingssource_get_field_value():
    class SettingsClass:
        model_config = {}

    fileconfigsettingssource = FileConfigSettingsSource(SettingsClass)
    field = FieldInfo(title="testfield")
    assert fileconfigsettingssource.get_field_value(field, "testfield") == (
        None,
        "",
        False,
    )
    assert fileconfigsettingssource.get_field_value(None, "a") == (None, "", False)


================================================
FILE: tests/test_initial.py
================================================
import pytest

from goodconf import Field, GoodConf, initial_for_field

from .utils import KEY


def test_initial():
    class C(GoodConf):
        f: str = Field(initial=lambda: "x")

    assert initial_for_field(KEY, C.model_fields["f"]) == "x"


def test_initial_bad():
    class C(GoodConf):
        f: str = Field(initial="x")

    with pytest.raises(ValueError, match="callable"):
        initial_for_field(KEY, C.model_fields["f"])


def test_initial_default():
    class C(GoodConf):
        f: str = Field("x")

    assert initial_for_field(KEY, C.model_fields["f"]) == "x"


def test_initial_default_factory():
    class C(GoodConf):
        f: str = Field(default_factory=lambda: "y")

    assert initial_for_field(KEY, C.model_fields["f"]) == "y"


def test_no_initial():
    class C(GoodConf):
        f: str = Field()

    assert initial_for_field(KEY, C.model_fields["f"]) == ""


def test_default_initial():
    """Can get initial when Field is not used"""

    class G(GoodConf):
        a: str = "test"

    initial = G().get_initial()
    assert initial["a"] == "test"


def test_optional_initial():
    class G(GoodConf):
        a: str | None

    initial = G().get_initial()
    assert initial["a"] is None


================================================
FILE: tests/utils.py
================================================
import os
from contextlib import contextmanager

KEY = "GOODCONF_TEST"


@contextmanager
def env_var(key, value):
    os.environ[key] = value
    try:
        yield
    finally:
        del os.environ[key]
Download .txt
gitextract_x6obglxq/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── build.yml
│       └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .yamllint.yml
├── CHANGES.rst
├── LICENSE
├── README.rst
├── goodconf/
│   ├── __init__.py
│   ├── contrib/
│   │   ├── __init__.py
│   │   ├── argparse.py
│   │   └── django.py
│   └── py.typed
├── pyproject.toml
└── tests/
    ├── __init__.py
    ├── test_django.py
    ├── test_file_helpers.py
    ├── test_files.py
    ├── test_goodconf.py
    ├── test_initial.py
    └── utils.py
Download .txt
SYMBOL INDEX (70 symbols across 9 files)

FILE: goodconf/__init__.py
  function Field (line 30) | def Field(
  class GoodConfConfigDict (line 43) | class GoodConfConfigDict(SettingsConfigDict):
  function _load_config (line 60) | def _load_config(path: str) -> dict[str, Any]:
  function _find_file (line 93) | def _find_file(filename: str, require: bool = True) -> str | None:
  function _fieldinfo_to_str (line 101) | def _fieldinfo_to_str(field_info: FieldInfo) -> str:
  function initial_for_field (line 121) | def initial_for_field(name: str, field_info: FieldInfo) -> Any:
  class FileConfigSettingsSource (line 140) | class FileConfigSettingsSource(PydanticBaseSettingsSource):
    method __init__ (line 145) | def __init__(self, settings_cls: type[BaseSettings]):
    method get_field_value (line 148) | def get_field_value(
    method __call__ (line 154) | def __call__(self) -> dict[str, Any]:
    method __repr__ (line 176) | def __repr__(self) -> str:
  class GoodConf (line 180) | class GoodConf(BaseSettings):
    method __init__ (line 181) | def __init__(
    method settings_customise_sources (line 198) | def settings_customise_sources(
    method _settings_build_values (line 218) | def _settings_build_values(
    method _load (line 230) | def _load(
    method load (line 240) | def load(self, filename: str | None = None) -> None:
    method get_initial (line 244) | def get_initial(cls, **override) -> dict:
    method generate_yaml (line 251) | def generate_yaml(cls, **override) -> str:
    method generate_json (line 280) | def generate_json(cls, **override) -> str:
    method generate_toml (line 287) | def generate_toml(cls, **override) -> str:
    method generate_markdown (line 307) | def generate_markdown(cls) -> str:
    method django_manage (line 331) | def django_manage(self, args: list[str] | None = None):

FILE: goodconf/contrib/argparse.py
  function argparser_add_argument (line 6) | def argparser_add_argument(parser: argparse.ArgumentParser, config: Good...

FILE: goodconf/contrib/django.py
  function load_config_from_cli (line 10) | def load_config_from_cli(
  function execute_from_command_line_with_config (line 39) | def execute_from_command_line_with_config(config: GoodConf, argv: list[s...

FILE: tests/test_django.py
  function test_mgmt_command (line 11) | def test_mgmt_command(mocker, tmpdir):
  function test_help (line 27) | def test_help(mocker, tmpdir, capsys):

FILE: tests/test_file_helpers.py
  function test_json (line 9) | def test_json(tmpdir):
  function test_load_toml (line 15) | def test_load_toml(tmpdir):
  function test_load_empty_toml (line 23) | def test_load_empty_toml(tmpdir):
  function test_yaml (line 31) | def test_yaml(tmpdir):
  function test_load_empty_yaml (line 38) | def test_load_empty_yaml(tmpdir):
  function test_missing (line 45) | def test_missing(tmpdir):
  function test_missing_strict (line 50) | def test_missing_strict(tmpdir):
  function test_abspath (line 56) | def test_abspath(tmpdir):
  function test_relative (line 63) | def test_relative(tmpdir):

FILE: tests/test_files.py
  function test_conf_env_var (line 8) | def test_conf_env_var(mocker, tmpdir):
  function test_conflict (line 22) | def test_conflict(tmpdir):
  function test_all_env_vars (line 39) | def test_all_env_vars(mocker):
  function test_provided_file (line 52) | def test_provided_file(mocker, tmpdir):
  function test_provided_file_from_init (line 65) | def test_provided_file_from_init(mocker, tmpdir):
  function test_default_files (line 78) | def test_default_files(mocker, tmpdir):

FILE: tests/test_goodconf.py
  function test_initial (line 15) | def test_initial():
  function test_dump_json (line 26) | def test_dump_json():
  function test_dump_toml (line 35) | def test_dump_toml():
  function test_dump_yaml (line 59) | def test_dump_yaml():
  function test_dump_yaml_no_docstring (line 87) | def test_dump_yaml_no_docstring():
  function test_dump_yaml_none (line 103) | def test_dump_yaml_none():
  function test_generate_markdown (line 113) | def test_generate_markdown():
  function test_generate_markdown_no_docstring (line 129) | def test_generate_markdown_no_docstring():
  function test_generate_markdown_default_false (line 141) | def test_generate_markdown_default_false():
  function test_generate_markdown_types (line 150) | def test_generate_markdown_types():
  function test_generate_markdown_required (line 162) | def test_generate_markdown_required():
  function test_undefined (line 170) | def test_undefined():
  function test_required_missing (line 176) | def test_required_missing():
  function test_default_values_are_used (line 189) | def test_default_values_are_used(monkeypatch):
  function test_set_on_init (line 212) | def test_set_on_init():
  function test_env_prefix (line 221) | def test_env_prefix():
  function test_precedence (line 233) | def test_precedence(tmpdir):
  function test_fileconfigsettingssource_repr (line 254) | def test_fileconfigsettingssource_repr():
  function test_fileconfigsettingssource_get_field_value (line 263) | def test_fileconfigsettingssource_get_field_value():

FILE: tests/test_initial.py
  function test_initial (line 8) | def test_initial():
  function test_initial_bad (line 15) | def test_initial_bad():
  function test_initial_default (line 23) | def test_initial_default():
  function test_initial_default_factory (line 30) | def test_initial_default_factory():
  function test_no_initial (line 37) | def test_no_initial():
  function test_default_initial (line 44) | def test_default_initial():
  function test_optional_initial (line 54) | def test_optional_initial():

FILE: tests/utils.py
  function env_var (line 8) | def env_var(key, value):
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (54K chars).
[
  {
    "path": ".editorconfig",
    "chars": 275,
    "preview": "# https://editorconfig.org/\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trail"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 2547,
    "preview": "---\nname: Publish to PyPI\n\"on\":\n  push:\n    branches: [main, test-publish]\n    tags: [\"*\"]\n  pull_request:\n\njobs:\n  buil"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1268,
    "preview": "---\nname: test\n\n\"on\":\n  pull_request:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\npermissions:\n  contents: read #"
  },
  {
    "path": ".gitignore",
    "chars": 117,
    "preview": "*.egg-info\n.coverage\n.pytest_cache\n/build\n/dist\n/goodconf/_version.py\n__pycache__\nenv\nhtmlcov\njunit.xml\nuv.lock\nvenv\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1524,
    "preview": "---\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: check-yaml\n  "
  },
  {
    "path": ".yamllint.yml",
    "chars": 98,
    "preview": "---\nextends: default\nrules:\n  comments:\n    min-spaces-from-content: 1\n  line-length:\n    max: 88\n"
  },
  {
    "path": "CHANGES.rst",
    "chars": 6368,
    "preview": "==========\nChange Log\n==========\n\n7.0.0 (3 March 2026)\n========================\n\n- **Backwards Incompatible Release**\n\n "
  },
  {
    "path": "LICENSE",
    "chars": 1056,
    "preview": "Copyright (c) 2018 Lincoln Loop\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this so"
  },
  {
    "path": "README.rst",
    "chars": 6211,
    "preview": "Goodconf\n========\n\n.. image:: https://github.com/lincolnloop/goodconf/actions/workflows/test.yml/badge.svg?branch=main&e"
  },
  {
    "path": "goodconf/__init__.py",
    "chars": 11447,
    "preview": "\"\"\"\nTransparently load variables from environment or JSON/YAML file.\n\"\"\"\n\nimport errno\nimport json\nimport logging\nimport"
  },
  {
    "path": "goodconf/contrib/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "goodconf/contrib/argparse.py",
    "chars": 633,
    "preview": "import argparse\n\nfrom .. import GoodConf\n\n\ndef argparser_add_argument(parser: argparse.ArgumentParser, config: GoodConf)"
  },
  {
    "path": "goodconf/contrib/django.py",
    "chars": 1425,
    "preview": "import argparse\nfrom collections.abc import Generator\nfrom contextlib import contextmanager\n\nfrom .. import GoodConf\nfro"
  },
  {
    "path": "goodconf/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pyproject.toml",
    "chars": 3491,
    "preview": "[build-system]\nbuild-backend = \"hatchling.build\"\nrequires = [\"hatchling\", \"hatch-vcs\"]\n\n[dependency-groups]\ndev = [\n  \"d"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_django.py",
    "chars": 1738,
    "preview": "import sys\n\nimport pytest\nfrom pydantic import ConfigDict\n\nfrom goodconf import GoodConf\n\npytest.importorskip(\"django\")\n"
  },
  {
    "path": "tests/test_file_helpers.py",
    "chars": 1594,
    "preview": "import os\nimport sys\n\nimport pytest\n\nfrom goodconf import _find_file, _load_config\n\n\ndef test_json(tmpdir):\n    conf = t"
  },
  {
    "path": "tests/test_files.py",
    "chars": 2062,
    "preview": "import json\n\nfrom goodconf import GoodConf\n\nfrom .utils import env_var\n\n\ndef test_conf_env_var(mocker, tmpdir):\n    mock"
  },
  {
    "path": "tests/test_goodconf.py",
    "chars": 6630,
    "preview": "import json\nimport os\nimport re\nfrom textwrap import dedent\nfrom typing import Literal\n\nimport pytest\nfrom pydantic impo"
  },
  {
    "path": "tests/test_initial.py",
    "chars": 1229,
    "preview": "import pytest\n\nfrom goodconf import Field, GoodConf, initial_for_field\n\nfrom .utils import KEY\n\n\ndef test_initial():\n   "
  },
  {
    "path": "tests/utils.py",
    "chars": 206,
    "preview": "import os\nfrom contextlib import contextmanager\n\nKEY = \"GOODCONF_TEST\"\n\n\n@contextmanager\ndef env_var(key, value):\n    os"
  }
]

About this extraction

This page contains the full source code of the lincolnloop/goodconf GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (48.7 KB), approximately 12.9k tokens, and a symbol index with 70 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!