[
  {
    "path": ".editorconfig",
    "content": "# https://editorconfig.org/\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nend_of_line = lf\ncharset = utf-8\nmax_line_length = 88\n\n[*.py]\nindent_size = 4\n\n[*.md]\nindent_size = 4\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "---\nname: Publish to PyPI\n\"on\":\n  push:\n    branches: [main, test-publish]\n    tags: [\"*\"]\n  pull_request:\n\njobs:\n  build:\n    name: Build distribution\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n      - name: Install hatch\n        run: pip install hatch\n      - name: Build a binary wheel and a source tarball\n        run: hatch build\n      - name: Store the distribution packages\n        uses: actions/upload-artifact@v4\n        with:\n          name: python-package-distributions\n          path: dist/\n\n  pypi-publish:\n    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')\n    needs: [build]\n    name: Upload release to PyPI\n    runs-on: ubuntu-latest\n    environment:\n      name: pypi\n      url: https://pypi.org/p/goodconf\n    permissions:\n      id-token: write\n    steps:\n      - name: Download all the dists\n        uses: actions/download-artifact@v4\n        with:\n          name: python-package-distributions\n          path: dist/\n      - name: Publish distribution to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n\n  github-release:\n    name: >-\n      Sign the Python 🐍 distribution 📦 with Sigstore\n      and upload them to GitHub Release\n    needs:\n      - pypi-publish\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write # IMPORTANT: mandatory for making GitHub Releases\n      id-token: write # IMPORTANT: mandatory for sigstore\n\n    steps:\n      - name: Download all the dists\n        uses: actions/download-artifact@v4\n        with:\n          name: python-package-distributions\n          path: dist/\n      - name: Sign the dists with Sigstore\n        uses: sigstore/gh-action-sigstore-python@v3.0.0\n        with:\n          inputs: >-\n            ./dist/*.tar.gz\n            ./dist/*.whl\n      - name: Create GitHub Release\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        run: >-\n          gh release create\n          '${{ github.ref_name }}'\n          --repo '${{ github.repository }}'\n          --generate-notes\n      - name: Upload artifact signatures to GitHub Release\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        # Upload to GitHub Release using the `gh` CLI.\n        # `dist/` contains the built packages, and the\n        # sigstore-produced signatures and certificates.\n        run: >-\n          gh release upload\n          '${{ github.ref_name }}' dist/**\n          --repo '${{ github.repository }}'\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "---\nname: test\n\n\"on\":\n  pull_request:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n    env:\n      PYTHON: ${{ matrix.python }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n\n      - name: Set up Python ${{ matrix.python }}\n        run: uv python install ${{ matrix.python }}\n\n      - name: Install dependencies\n        run: uv sync --group dev --python ${{ matrix.python }}\n\n      - name: Test\n        run: uv run pytest --cov-report=xml --cov-report=term --junitxml=junit.xml\n\n      - name: Upload coverage to Codecov\n        if: ${{ (success() || failure()) && matrix.python == '3.14' }}\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          fail_ci_if_error: true\n\n      - name: Upload test results to Codecov\n        if: ${{ (success() || failure()) && matrix.python == '3.14' }}\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          report_type: test_results\n          fail_ci_if_error: true\n"
  },
  {
    "path": ".gitignore",
    "content": "*.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",
    "content": "---\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: check-yaml\n      - id: end-of-file-fixer\n      - id: mixed-line-ending\n      - id: trailing-whitespace\n      - id: file-contents-sorter\n        files: (.dockerignore|.gitignore)\n\n  - repo: https://github.com/adrienverge/yamllint.git\n    rev: v1.38.0\n    hooks:\n      - id: yamllint\n        args: [--strict]\n\n  - repo: https://github.com/python-jsonschema/check-jsonschema\n    rev: 0.37.0\n    hooks:\n      - id: check-github-workflows\n\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.15.5\n    hooks:\n      - id: ruff-check\n        args: [--fix]\n      - id: ruff-format\n\n  - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks\n    rev: v2.16.0\n    hooks:\n      - id: pretty-format-toml\n        args: [--autofix, --trailing-commas]\n        types: [toml]\n        files: \\.toml\n\n  - repo: https://github.com/ninoseki/uv-sort\n    rev: v0.7.0\n    hooks:\n      - id: uv-sort\n\n  - repo: local\n    hooks:\n      - id: prettier\n        name: prettier\n        language: node\n        entry: prettier\n        args: [--write, --ignore-unknown]\n        additional_dependencies: [prettier@3]\n        exclude_types: [python]\n        pass_filenames: true\n\n  - repo: local\n    hooks:\n      - id: mypy\n        name: mypy\n        language: python\n        entry: mypy goodconf\n        types: [python]\n        require_serial: true\n        pass_filenames: false\n        additional_dependencies: [mypy]\n"
  },
  {
    "path": ".yamllint.yml",
    "content": "---\nextends: default\nrules:\n  comments:\n    min-spaces-from-content: 1\n  line-length:\n    max: 88\n"
  },
  {
    "path": "CHANGES.rst",
    "content": "==========\nChange Log\n==========\n\n7.0.0 (3 March 2026)\n========================\n\n- **Backwards Incompatible Release**\n\n  - Removed official support for Python 3.9\n  - Added official support for Python 3.13 and 3.14\n  - Updated pydantic-settings requirement to >=2.13 (was >=2.4)\n\n6.0.0 (8 October 2024)\n========================\n\n- **Backwards Incompatible Release**\n\n  - Removed the ``_config_file`` attribute from ``GoodConf``.\n    If you previously set this attribute, you are no longer be able to do so.\n- Support reading TOML files via ``tomllib`` on Python 3.11+\n- Update Markdown generation, so that output matches v4 output\n\n5.0.0 (13 August 2024)\n========================\n\n- **Backwards Incompatible Release**\n\n  - Removed official support for Python 3.8\n  - upgraded to pydantic2\n\n  To upgrade from goodconf v4 to goodconf v5:\n\n  - If subclassing ``GoodConf``, replace uses of ``class Config`` with ``model_config``.\n\n    For example goodconf v4 code like this:\n\n    .. code:: python\n\n        from goodconf import GoodConf\n\n        class AppConfig(GoodConf):\n            \"Configuration for My App\"\n            DATABASE_URL: PostgresDsn = \"postgres://localhost:5432/mydb\"\n\n            class Config:\n                default_files = [\"/etc/myproject/myproject.yaml\", \"myproject.yaml\"]\n\n        config = AppConfig()\n\n    should be replaced in goodconf v5 with:\n\n    .. code:: python\n\n        from goodconf import GoodConf\n\n        class AppConfig(GoodConf):\n            \"Configuration for My App\"\n            DATABASE_URL: PostgresDsn = \"postgres://localhost:5432/mydb\"\n\n            model_config = {\"default_files\": [\"/etc/myproject/myproject.yaml\", \"myproject.yaml\"]}\n\n        config = AppConfig()\n\n4.0.3 (13 August 2024)\n========================\n\n- Release from GitHub Actions\n\n4.0.2 (11 February 2024)\n========================\n\n- Another markdown output fix\n- Fix for markdown generation generation on Python 3.8 & 3.9\n\n4.0.1 (10 February 2024)\n========================\n\n- Fix trailing whitespace in markdown output\n\n4.0.0 (10 February 2024)\n========================\n\n- Removed errant print statement\n- Removed official support for Python 3.7\n- Added support for Python 3.12\n\n3.1.0 (10 February 2024)\n========================\n\n- Fixed type display in Markdown generation\n- Changed markdown output format (trailing spaces were problematic).\n\n3.0.1 (30 June 2023)\n====================\n\n- pin to pydantic < 2 due to breaking changes in 2.0\n\n3.0.0 (17 January 2023)\n==================\n\n- TOML files are now supported as configuration source\n- Python 3.11 and 3.10 are now officially supported\n- Python 3.6 is no longer officially supported\n- Requires Pydantic 1.7+\n- Variables can now be set during class initialization\n\n\n2.0.1 (15 June 2021)\n====================\n\n- Change to newer syntax for safe loading yaml\n\n\n2.0.0 (13 May 2021)\n===================\n\n- **Backwards Incompatible Release**\n    Internals replaced with `pydantic <https://pypi.org/project/pydantic/>`_. Users can either pin to ``1.0.0`` or update their code as follows:\n\n    - Replace ``goodconf.Value`` with ``goodconf.Field``.\n    - Replace ``help`` keyword argument with ``description`` in ``Field`` (previously ``Value``).\n    - Remove ``cast_as`` keyword argument from ``Field`` (previously ``Value``). Standard Python type annotations are now used.\n    - Move ``file_env_var`` and ``default_files`` keyword arguments used in class initialization to a sub-class named ``Config``\n\n    Given a version ``1`` class that looks like this:\n\n    .. code:: python\n\n        from goodconf import GoodConf, Value\n\n        class AppConfig(GoodConf):\n            \"Configuration for My App\"\n            DEBUG = Value(default=False, help=\"Toggle debugging.\")\n            MAX_REQUESTS = Value(cast_as=int)\n\n        config = AppConfig(default_files=[\"config.yml\"])\n\n    A class updated for version `2` would be:\n\n    .. code:: python\n\n        from goodconf import GoodConf, Field\n\n        class AppConfig(GoodConf):\n            \"Configuration for My App\"\n            DEBUG: bool = Field(default=False, description=\"Toggle debugging.\")\n            MAX_REQUESTS: int\n\n            class Config:\n                default_files=[\"config.yml\"]\n\n        config = AppConfig()\n\n2.0b3 (15 April 2021)\n=====================\n\n- Environment variables take precedence over configuration files in the event of a conflict\n\n2.0b2 (12 March 2021)\n=====================\n\n- Use null value for initial if allowed\n- Store the config file parsed as ``GoodConf.Config._config_file``\n\n\n2.0b1 (11 March 2021)\n=====================\n\n- Backwards Incompatible: Migrated backend to ``pydantic``.\n\n  - ``Value`` is replaced by the `Field function <https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation>`__.\n  - ``help`` keyword arg is now ``description``\n  - ``GoodConf`` is now backed by `BaseSettings <https://pydantic-docs.helpmanual.io/usage/settings/>`__\n    Instead of passing keyword args when instantiating the class, they are now defined on a ``Config`` class on the object\n\n\n\n1.0.0 (18 July 2018)\n====================\n\n- Allow overriding of values in the generate_* methods\n- Python 3.7 supported\n\n\n0.9.1 (10 April 2018)\n=====================\n\n- Explicit ``load`` method\n- ``django_manage`` method helper on ``GoodConf``\n- Fixed a few minor bugs\n\n\n0.9.0 (8 April 2018)\n====================\n\n- Use a declarative class to define GoodConf's values.\n\n- Change description to a docstring of the class.\n\n- Remove the redundant ``required`` argument from ``Values``. To make\n  an value optional, give it a default.\n\n- Changed implicit loading to happen during instanciation rather than first\n  access. Instanciate with ``load=False`` to avoid loading config initially.\n\n0.8.3 (28 Mar 2018)\n===================\n\n- Implicitly load config if not loaded by first access.\n\n0.8.2 (28 Mar 2018)\n===================\n\n- ``-c`` is used by Django's ``collectstatic``. Using ``-C`` instead.\n\n0.8.1 (28 Mar 2018)\n===================\n\n- Adds ``goodconf.contrib.argparse`` to add a config argument to an existing\n  parser.\n\n0.8.0 (27 Mar 2018)\n===================\n\n- Major refactor from ``file-or-env`` to ``goodconf``\n\n0.6.1 (16 Mar 2018)\n================\n\n- Fixed packaging issue.\n\n0.6.0 (16 Mar 2018)\n================\n\n- Fixes bug in stack traversal to find calling file.\n\n\n0.5.1 (15 March 2018)\n==================\n\n- Initial release\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2018 Lincoln Loop\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.rst",
    "content": "Goodconf\n========\n\n.. image:: https://github.com/lincolnloop/goodconf/actions/workflows/test.yml/badge.svg?branch=main&event=push\n    :target: https://github.com/lincolnloop/goodconf/actions/workflows/test.yml?query=branch%3Amain+event%3Apush\n\n.. image:: https://results.pre-commit.ci/badge/github/lincolnloop/goodconf/main.svg\n    :target: https://results.pre-commit.ci/latest/github/lincolnloop/goodconf/main\n    :alt: pre-commit.ci status\n\n.. image:: https://img.shields.io/codecov/c/github/lincolnloop/goodconf.svg\n    :target: https://codecov.io/gh/lincolnloop/goodconf\n\n.. image:: https://img.shields.io/pypi/v/goodconf.svg\n    :target: https://pypi.python.org/pypi/goodconf\n\n.. image:: https://img.shields.io/pypi/pyversions/goodconf.svg\n    :target: https://pypi.python.org/pypi/goodconf\n\nA thin wrapper over `Pydantic's settings management <https://pydantic-docs.helpmanual.io/usage/settings/>`__.\nAllows you to define configuration variables and load them from environment or JSON/YAML/TOML\nfile. Also generates initial configuration files and documentation for your\ndefined configuration.\n\n\nInstallation\n------------\n\n``pip install goodconf`` or ``pip install goodconf[yaml]`` /\n``pip install goodconf[toml]`` if parsing/generating YAML/TOML\nfiles is required. When running on Python 3.11+ the ``[toml]``\nextra is only required for generating TOML files as parsing\nis supported natively.\n\n\nQuick Start\n-----------\n\nLet's use configurable Django settings as an example.\n\nFirst, create a ``conf.py`` file in your project's directory, next to\n``settings.py``:\n\n.. code:: python\n\n    import base64\n    import os\n\n    from goodconf import GoodConf, Field\n    from pydantic import PostgresDsn\n\n    class AppConfig(GoodConf):\n        \"Configuration for My App\"\n        DEBUG: bool\n        DATABASE_URL: PostgresDsn = \"postgres://localhost:5432/mydb\"\n        SECRET_KEY: str = Field(\n            initial=lambda: base64.b64encode(os.urandom(60)).decode(),\n            description=\"Used for cryptographic signing. \"\n            \"https://docs.djangoproject.com/en/2.0/ref/settings/#secret-key\")\n\n        model_config = {\"default_files\": [\"/etc/myproject/myproject.yaml\", \"myproject.yaml\"]}\n\n    config = AppConfig()\n\nNext, use the config in your ``settings.py`` file:\n\n.. code:: python\n\n    import dj_database_url\n    from .conf import config\n\n    config.load()\n\n    DEBUG = config.DEBUG\n    SECRET_KEY = config.SECRET_KEY\n    DATABASES = {\"default\": dj_database_url.parse(config.DATABASE_URL)}\n\nIn your initial developer installation instructions, give some advice such as:\n\n.. code:: shell\n\n    python -c \"import myproject; print(myproject.conf.config.generate_yaml(DEBUG=True))\" > myproject.yaml\n\nBetter yet, make it a function and `entry point <https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation>`__ so you can install\nyour project and run something like ``generate-config > myproject.yaml``.\n\nUsage\n-----\n\n\n``GoodConf``\n^^^^^^^^^^^^\n\nYour subclassed ``GoodConf`` object can include a ``model_config`` dictionary with the following\nattributes:\n\n``file_env_var``\n  The name of an environment variable which can be used for\n  the name of the configuration file to load.\n``default_files``\n  If no file is passed to the ``load`` method, try to load a\n  configuration from these files in order.\n\nIt also has one method:\n\n``load``\n  Trigger the load method during instantiation. Defaults to False.\n\nUse plain-text docstring for use as a header when generating a configuration\nfile.\n\nEnvironment variables always take precedence over variables in the configuration files.\n\nSee Pydantic's docs for examples of loading:\n\n* `Dotenv (.env) files <https://pydantic-docs.helpmanual.io/usage/settings/#dotenv-env-support>`_\n* `Docker secrets <https://pydantic-docs.helpmanual.io/usage/settings/#secret-support>`_\n\n\nFields\n^^^^^^\n\nDeclare configuration values by subclassing ``GoodConf`` and defining class\nattributes which are standard Python type definitions or Pydantic ``FieldInfo``\ninstances generated by the ``Field`` function.\n\nGoodconf can use one extra argument provided to the ``Field`` to define an function\nwhich can generate an initial value for the field:\n\n``initial``\n  Callable to use for initial value when generating a config\n\n\nDjango Usage\n------------\n\nA helper is provided which monkey-patches Django's management commands to\naccept a ``--config`` argument. Replace your ``manage.py`` with the following:\n\n.. code:: python\n\n    # Define your GoodConf in `myproject/conf.py`\n    from myproject.conf import config\n\n    if __name__ == '__main__':\n        config.django_manage()\n\n\nWhy?\n----\n\nI took inspiration from `logan <https://github.com/dcramer/logan>`__ (used by\nSentry) and `derpconf <https://github.com/globocom/derpconf>`__ (used by\nThumbor). Both, however used Python files for configuration. I wanted a safer\nformat and one that was easier to serialize data into from a configuration\nmanagement system.\n\nEnvironment Variables\n^^^^^^^^^^^^^^^^^^^^^\n\nI don't like working with environment variables. First, there are potential\nsecurity issues:\n\n1. Accidental leaks via logging or error reporting services.\n2. Child process inheritance (see `ImageTragick <https://imagetragick.com/>`__\n   for an idea why this could be bad).\n\nSecond, in practice on deployment environments, environment variables end up\ngetting written to a number of files (cron, bash profile, service definitions,\nweb server config, etc.). Not only is it cumbersome, but also increases the\npossibility of leaks via incorrect file permissions.\n\nI prefer a single structured file which is explicitly read by the application.\nI also want it to be easy to run my applications on services like Heroku\nwhere environment variables are the preferred configuration method.\n\nThis module let's me do things the way I prefer in environments I control, but\nstill run them with environment variables on environments I don't control with\nminimal fuss.\n\n\nContribute\n----------\n\nInstall dependencies.\n\n.. code:: shell\n\n    uv sync\n\n\nRun tests\n\n.. code:: shell\n\n    uv run pytest\n\nReleases are done with GitHub Actions whenever a new tag is created. For more information,\nsee `<./.github/workflows/build.yml>`_\n"
  },
  {
    "path": "goodconf/__init__.py",
    "content": "\"\"\"\nTransparently load variables from environment or JSON/YAML file.\n\"\"\"\n\nimport errno\nimport json\nimport logging\nimport os\nimport sys\nfrom functools import partial\nfrom io import StringIO\nfrom types import GenericAlias\nfrom typing import Any, cast, get_args\n\nfrom pydantic._internal._config import config_keys\nfrom pydantic.fields import Field as PydanticField\nfrom pydantic.fields import FieldInfo, PydanticUndefined\nfrom pydantic.main import _object_setattr\nfrom pydantic_settings import (\n    BaseSettings,\n    PydanticBaseSettingsSource,\n    SettingsConfigDict,\n)\n\n__all__ = [\"Field\", \"GoodConf\", \"GoodConfConfigDict\"]\n\nlog = logging.getLogger(__name__)\n\n\ndef Field(\n    *args,\n    initial=None,\n    json_schema_extra=None,\n    **kwargs,\n):\n    if initial:\n        json_schema_extra = json_schema_extra or {}\n        json_schema_extra[\"initial\"] = initial\n\n    return PydanticField(*args, json_schema_extra=json_schema_extra, **kwargs)\n\n\nclass GoodConfConfigDict(SettingsConfigDict):\n    # configuration file to load\n    file_env_var: str | None\n    # if no file is given, try to load a configuration from these files in order\n    default_files: list[str] | None\n\n\n# Note: code from pydantic-settings/pydantic_settings/main.py:\n# Extend `config_keys` by pydantic settings config keys to\n# support setting config through class kwargs.\n# Pydantic uses `config_keys` in `pydantic._internal._config.ConfigWrapper.for_model`\n# to extract config keys from model kwargs, So, by adding pydantic settings keys to\n# `config_keys`, they will be considered as valid config keys and will be collected\n# by Pydantic.\nconfig_keys |= set(GoodConfConfigDict.__annotations__.keys())\n\n\ndef _load_config(path: str) -> dict[str, Any]:\n    \"\"\"\n    Given a file path, parse it based on its extension (YAML, TOML or JSON)\n    and return the values as a Python dictionary. JSON is the default if an\n    extension can't be determined.\n    \"\"\"\n    __, ext = os.path.splitext(path)\n    if ext in [\".yaml\", \".yml\"]:\n        import ruamel.yaml\n\n        yaml = ruamel.yaml.YAML(typ=\"safe\", pure=True)\n        loader = yaml.load\n    elif ext == \".toml\":\n        try:\n            import tomllib\n\n            def load(stream):\n                return tomllib.loads(f.read())\n        except ImportError:  # Fallback for Python < 3.11\n            import tomlkit\n\n            def load(stream):\n                return tomlkit.load(f).unwrap()\n\n        loader = load\n\n    else:\n        loader = json.load\n    with open(path) as f:\n        config = loader(f)\n    return config or {}\n\n\ndef _find_file(filename: str, require: bool = True) -> str | None:\n    if not os.path.exists(filename):\n        if not require:\n            return None\n        raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), filename)\n    return os.path.abspath(filename)\n\n\ndef _fieldinfo_to_str(field_info: FieldInfo) -> str:\n    \"\"\"\n    Return the string representation of a pydantic.fields.FieldInfo.\n    \"\"\"\n    if isinstance(field_info.annotation, type) and not isinstance(\n        field_info.annotation, GenericAlias\n    ):\n        # For annotation like <class 'int'>, we use its name (\"int\").\n        field_type = field_info.annotation.__name__\n    elif str(field_info.annotation).startswith(\"typing.\"):\n        # For annotation like typing.Literal['a', 'b'], we use\n        # its string representation, but without \"typing.\" (\"Literal['a', 'b']\").\n        field_type = str(field_info.annotation)[len(\"typing.\") :]\n    else:\n        # For annotation like list[str], we use its string\n        # representation (\"list[str]\").\n        field_type = field_info.annotation\n    return field_type\n\n\ndef initial_for_field(name: str, field_info: FieldInfo) -> Any:\n    try:\n        json_schema_extra = field_info.json_schema_extra or {}\n        if not callable(json_schema_extra[\"initial\"]):\n            raise ValueError(f\"Initial value for `{name}` must be a callable.\")\n        return field_info.json_schema_extra[\"initial\"]()\n    except KeyError:\n        if (\n            field_info.default is not PydanticUndefined\n            and field_info.default is not ...\n        ):\n            return field_info.default\n        if field_info.default_factory is not None:\n            return field_info.default_factory()\n    if type(None) in get_args(field_info.annotation):\n        return None\n    return \"\"\n\n\nclass FileConfigSettingsSource(PydanticBaseSettingsSource):\n    \"\"\"\n    Source class for loading values provided during settings class initialization.\n    \"\"\"\n\n    def __init__(self, settings_cls: type[BaseSettings]):\n        super().__init__(settings_cls)\n\n    def get_field_value(\n        self, field: FieldInfo, field_name: str\n    ) -> tuple[Any, str, bool]:\n        # Nothing to do here. Only implement the return statement to make mypy happy\n        return None, \"\", False\n\n    def __call__(self) -> dict[str, Any]:\n        settings = cast(\"GoodConf\", self.settings_cls)\n        selected_config_file = None\n        if cfg_file := self.current_state.get(\"_config_file\"):\n            selected_config_file = cfg_file\n        elif (file_env_var := settings.model_config.get(\"file_env_var\")) and (\n            cfg_file := os.environ.get(file_env_var)\n        ):\n            selected_config_file = _find_file(cfg_file)\n        else:\n            for filename in settings.model_config.get(\"default_files\") or []:\n                selected_config_file = _find_file(filename, require=False)\n                if selected_config_file:\n                    break\n        if selected_config_file:\n            values = _load_config(selected_config_file)\n            log.info(\"Loading config from %s\", selected_config_file)\n        else:\n            values = {}\n            log.info(\"No config file specified. Loading with environment variables.\")\n        return values\n\n    def __repr__(self) -> str:\n        return \"FileConfigSettingsSource()\"\n\n\nclass GoodConf(BaseSettings):\n    def __init__(\n        self, load: bool = False, config_file: str | None = None, **kwargs\n    ) -> None:\n        \"\"\"\n        :param load: load config file on instantiation [default: False].\n\n        A docstring defined on the class should be a plain-text description\n        used as a header when generating a configuration file.\n        \"\"\"\n        if kwargs or load:  # Emulate Pydantic behavior, load immediately\n            self._load(_init_config_file=config_file, **kwargs)\n        elif config_file:\n            _object_setattr(\n                self, \"_load\", partial(self._load, _init_config_file=config_file)\n            )\n\n    @classmethod\n    def settings_customise_sources(\n        cls,\n        settings_cls: type[BaseSettings],\n        init_settings: PydanticBaseSettingsSource,\n        env_settings: PydanticBaseSettingsSource,\n        dotenv_settings: PydanticBaseSettingsSource,\n        file_secret_settings: PydanticBaseSettingsSource,\n    ) -> tuple[PydanticBaseSettingsSource, ...]:\n        \"\"\"Load environment variables before init\"\"\"\n        return (\n            init_settings,\n            env_settings,\n            dotenv_settings,\n            FileConfigSettingsSource(settings_cls),\n            file_secret_settings,\n        )\n\n    model_config = GoodConfConfigDict()\n\n    @classmethod\n    def _settings_build_values(\n        cls,\n        sources: tuple[PydanticBaseSettingsSource, ...],\n        init_kwargs: dict[str, Any],\n    ) -> dict[str, Any]:\n        state = super()._settings_build_values(\n            sources,\n            init_kwargs,\n        )\n        state.pop(\"_config_file\", None)\n        return state\n\n    def _load(\n        self,\n        _config_file: str | None = None,\n        _init_config_file: str | None = None,\n        **kwargs,\n    ):\n        if config_file := _config_file or _init_config_file:\n            kwargs[\"_config_file\"] = config_file\n        super().__init__(**kwargs)\n\n    def load(self, filename: str | None = None) -> None:\n        self._load(_config_file=filename)\n\n    @classmethod\n    def get_initial(cls, **override) -> dict:\n        return {\n            k: override.get(k, initial_for_field(k, v))\n            for k, v in cls.model_fields.items()\n        }\n\n    @classmethod\n    def generate_yaml(cls, **override) -> str:\n        \"\"\"\n        Dumps initial config in YAML\n        \"\"\"\n        import ruamel.yaml\n\n        yaml = ruamel.yaml.YAML()\n        yaml.representer.add_representer(\n            type(None),\n            lambda self, d: self.represent_scalar(\"tag:yaml.org,2002:null\", \"~\"),  # noqa: ARG005\n        )\n        yaml_str = StringIO()\n        yaml.dump(cls.get_initial(**override), stream=yaml_str)\n        yaml_str.seek(0)\n        dict_from_yaml = yaml.load(yaml_str)\n        if cls.__doc__:\n            dict_from_yaml.yaml_set_start_comment(\"\\n\" + cls.__doc__ + \"\\n\\n\")\n        for k in dict_from_yaml:\n            if cls.model_fields[k].description:\n                description = cast(\"str\", cls.model_fields[k].description)\n                dict_from_yaml.yaml_set_comment_before_after_key(\n                    k, before=\"\\n\" + description\n                )\n        yaml_str = StringIO()\n        yaml.dump(dict_from_yaml, yaml_str)\n        yaml_str.seek(0)\n        return yaml_str.read()\n\n    @classmethod\n    def generate_json(cls, **override) -> str:\n        \"\"\"\n        Dumps initial config in JSON\n        \"\"\"\n        return json.dumps(cls.get_initial(**override), indent=2)\n\n    @classmethod\n    def generate_toml(cls, **override) -> str:\n        \"\"\"\n        Dumps initial config in TOML\n        \"\"\"\n        import tomlkit\n        from tomlkit.items import Item\n\n        toml_str = tomlkit.dumps(cls.get_initial(**override))\n        dict_from_toml = tomlkit.loads(toml_str)\n        document = tomlkit.document()\n        if cls.__doc__:\n            document.add(tomlkit.comment(cls.__doc__))\n        for k, v in dict_from_toml.unwrap().items():\n            document.add(k, v)\n            if cls.model_fields[k].description:\n                description = cast(\"str\", cls.model_fields[k].description)\n                cast(\"Item\", document[k]).comment(description)\n        return tomlkit.dumps(document)\n\n    @classmethod\n    def generate_markdown(cls) -> str:\n        \"\"\"\n        Documents values in markdown\n        \"\"\"\n        lines = []\n        if cls.__doc__:\n            lines.extend([f\"# {cls.__doc__}\", \"\"])\n\n        for k, field_info in cls.model_fields.items():\n            lines.append(f\"* **{k}**\")\n            if field_info.is_required():\n                lines[-1] = f\"{lines[-1]} _REQUIRED_\"\n            if field_info.description:\n                lines.append(f\"  * description: {field_info.description}\")\n            # We want to append a line with the field_info type, and sometimes\n            # field_info.annotation looks the way we want, like 'list[str]', but\n            # other times, it includes some extra text, like '<class 'bool'>'.\n            # Therefore, we have some logic to make the type show up the way we want.\n            field_type = _fieldinfo_to_str(field_info)\n            lines.append(f\"  * type: `{field_type}`\")\n            if field_info.default not in [None, PydanticUndefined]:\n                lines.append(f\"  * default: `{field_info.default}`\")\n        return \"\\n\".join(lines)\n\n    def django_manage(self, args: list[str] | None = None):\n        args = args or sys.argv\n        from .contrib.django import execute_from_command_line_with_config\n\n        execute_from_command_line_with_config(self, args)\n"
  },
  {
    "path": "goodconf/contrib/__init__.py",
    "content": ""
  },
  {
    "path": "goodconf/contrib/argparse.py",
    "content": "import argparse\n\nfrom .. import GoodConf\n\n\ndef argparser_add_argument(parser: argparse.ArgumentParser, config: GoodConf):\n    \"\"\"Adds argument for config to existing argparser\"\"\"\n    help = \"Config file.\"\n    cfg = config.model_config\n    if cfg.get(\"file_env_var\"):\n        help += (\n            \"Can also be configured via the environment variable: \"\n            f\"{cfg['file_env_var']}\"\n        )\n    if cfg.get(\"default_files\"):\n        files_str = \", \".join(cfg[\"default_files\"])\n        help += f\" Defaults to the first file that exists from [{files_str}].\"\n    parser.add_argument(\"-C\", \"--config\", metavar=\"FILE\", help=help)\n"
  },
  {
    "path": "goodconf/contrib/django.py",
    "content": "import argparse\nfrom collections.abc import Generator\nfrom contextlib import contextmanager\n\nfrom .. import GoodConf\nfrom .argparse import argparser_add_argument\n\n\n@contextmanager\ndef load_config_from_cli(\n    config: GoodConf, argv: list[str]\n) -> Generator[list[str], None, None]:\n    \"\"\"Loads config, checking CLI arguments for a config file\"\"\"\n\n    # Monkey patch Django's command parser\n    from django.core.management.base import BaseCommand\n\n    original_parser = BaseCommand.create_parser\n\n    def patched_parser(self, prog_name, subcommand):\n        parser = original_parser(self, prog_name, subcommand)\n        argparser_add_argument(parser, config)\n        return parser\n\n    BaseCommand.create_parser = patched_parser\n\n    try:\n        parser = argparse.ArgumentParser(add_help=False)\n        argparser_add_argument(parser, config)\n\n        config_arg, default_args = parser.parse_known_args(argv)\n        config.load(config_arg.config)\n        yield default_args\n    finally:\n        # Put that create_parser back where it came from or so help me!\n        BaseCommand.create_parser = original_parser\n\n\ndef execute_from_command_line_with_config(config: GoodConf, argv: list[str]):\n    \"\"\"Load's config then runs Django's execute_from_command_line\"\"\"\n    with load_config_from_cli(config, argv) as args:\n        from django.core.management import execute_from_command_line\n\n        execute_from_command_line(args)\n"
  },
  {
    "path": "goodconf/py.typed",
    "content": ""
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nbuild-backend = \"hatchling.build\"\nrequires = [\"hatchling\", \"hatch-vcs\"]\n\n[dependency-groups]\ndev = [\n  \"django>=3.2.0\",\n  \"mypy>=1.19.1\",\n  \"pytest-cov==7.0.*\",\n  \"pytest-mock==3.15.*\",\n  \"pytest==9.0.*\",\n  \"ruamel.yaml>=0.17.0\",\n  \"tomlkit>=0.11.6\",\n]\n\n[project]\nauthors = [\n  {name = \"Peter Baumgartner\", email = \"brett@python.org\"},\n]\nclassifiers = [\n  \"Development Status :: 5 - Production/Stable\",\n  \"Intended Audience :: Developers\",\n  \"License :: OSI Approved :: MIT License\",\n  \"Operating System :: OS Independent\",\n  \"Programming Language :: Python\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\",\n]\ndependencies = [\n  \"pydantic-settings>=2.13\",\n  \"pydantic>=2.7\",\n]\ndescription = \"Load configuration variables from a file or environment\"\ndynamic = [\"version\"]\nkeywords = [\"env\", \"config\", \"json\", \"yaml\", \"toml\"]\nlicense = {file = \"LICENSE\"}\nname = \"goodconf\"\nreadme = \"README.rst\"\nrequires-python = \">=3.10\"\n\n[project.optional-dependencies]\ntoml = [\"tomlkit>=0.11.6\"]\nyaml = [\"ruamel.yaml>=0.17.0\"]\n\n[project.urls]\nchangelog = \"https://github.com/lincolnloop/goodconf/blob/main/CHANGES.rst\"\nhomepage = \"https://github.com/lincolnloop/goodconf/\"\n\n[tool.coverage.report]\nexclude_lines = [\n  \"if TYPE_CHECKING:\",\n  \"pragma: no cover\",\n  \"raise NotImplementedError\",\n]\nskip_covered = true\n\n[tool.coverage.run]\nbranch = true\ndisable_warnings = [\n  \"module-not-imported\",\n  \"module-not-measured\",\n  \"no-data-collected\",\n]\nsource = [\"goodconf\", \"tests\"]\n\n[tool.hatch.build.hooks.vcs]\nversion-file = \"goodconf/_version.py\"\n\n[tool.hatch.build.targets.sdist]\nexclude = [\n  \"/.github\",\n]\n\n[tool.hatch.version]\nsource = \"vcs\"\n\n[tool.mypy]\nignore_errors = true\n\n[tool.pytest.ini_options]\naddopts = [\"--cov\", \"--strict-markers\"]\n\n[tool.ruff.lint]\nignore = [\n  # Permanently suppressed\n  \"A001\",  # Variable shadowing builtin (intentional: `help` in argparse)\n  \"ANN401\",  # Dynamically typed expressions (typing.Any) are disallowed in `**kwargs`\n  \"ARG001\",  # Unused function argument\n  \"ARG002\",  # Unused method argument\n  \"COM812\",  # (ruff format) Checks for the absence of trailing commas\n  \"D\",  # Missing or badly formatted docstrings\n  \"E501\",  # Let the formatter handle long lines\n  \"FBT\",  # Flake Boolean Trap (don't use arg=True in functions)\n  \"ISC001\",  # (ruff format) Checks for implicitly concatenated strings on a single line\n  \"N802\",  # Function name should be lowercase (Field is a public API name)\n  \"PLC0415\",  # Import not at top level (intentional: lazy imports for optional deps)\n  \"RUF012\",  # Mutable class attributes https://github.com/astral-sh/ruff/issues/5243\n  # Temporary — see docs/plans/ruff-cleanup.md\n  \"ANN\",  # Missing type annotations (annotation work deferred)\n  \"EM102\",  # Exception message in f-string\n  \"PTH\",  # Use pathlib instead of os.path (refactoring deferred)\n  \"TC002\",  # Move import to TYPE_CHECKING block\n  \"TID252\",  # Relative imports\n  \"TRY003\",  # Long exception messages\n  \"TRY004\",  # Prefer TypeError over AttributeError\n]\nselect = [\"ALL\"]\n\n[tool.ruff.lint.extend-per-file-ignores]\n\"test_*.py\" = [\n  \"ANN001\",  # Missing type annotation for function argument\n  \"ANN201\",  # Missing return type annotation\n  \"PLR2004\",  # Magic value used in comparison\n  \"S101\",  # Use of `assert` detected\n  \"S105\",  # Hardcoded password (test fixtures)\n]\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_django.py",
    "content": "import sys\n\nimport pytest\nfrom pydantic import ConfigDict\n\nfrom goodconf import GoodConf\n\npytest.importorskip(\"django\")\n\n\ndef test_mgmt_command(mocker, tmpdir):\n    mocked_load_config = mocker.patch(\"goodconf._load_config\")\n    mocked_dj_execute = mocker.patch(\"django.core.management.execute_from_command_line\")\n    temp_config = tmpdir.join(\"config.yml\")\n    temp_config.write(\"\")\n\n    class G(GoodConf):\n        model_config = ConfigDict()\n\n    c = G()\n    dj_args = [\"manage.py\", \"diffsettings\", \"-v\", \"2\"]\n    c.django_manage([*dj_args, \"-C\", str(temp_config)])\n    mocked_load_config.assert_called_once_with(str(temp_config))\n    mocked_dj_execute.assert_called_once_with(dj_args)\n\n\ndef test_help(mocker, tmpdir, capsys):\n    mocker.patch(\"sys.exit\")\n    mocked_load_config = mocker.patch(\"goodconf._load_config\")\n    temp_config = tmpdir.join(\"config.yml\")\n    temp_config.write(\"\")\n\n    class G(GoodConf):\n        model_config = ConfigDict(\n            file_env_var=\"MYAPP_CONF\",\n            default_files=[\"/etc/myapp.json\"],\n        )\n\n    c = G()\n    assert c.model_config.get(\"file_env_var\") == \"MYAPP_CONF\"\n    c.django_manage(\n        [\n            \"manage.py\",\n            \"diffsettings\",\n            \"-C\",\n            str(temp_config),\n            \"--settings\",\n            __name__,\n            \"-h\",\n        ]\n    )\n    mocked_load_config.assert_called_once_with(str(temp_config))\n    output = capsys.readouterr()\n    if sys.version_info < (3, 13):\n        assert \"-C FILE, --config FILE\" in output.out\n    else:\n        assert \"-C, --config FILE\" in output.out\n\n    assert \"MYAPP_CONF\" in output.out\n    assert \"/etc/myapp.json\" in output.out\n\n\n# This doubles as a Django settings file for the tests\nSECRET_KEY = \"abc\"\n"
  },
  {
    "path": "tests/test_file_helpers.py",
    "content": "import os\nimport sys\n\nimport pytest\n\nfrom goodconf import _find_file, _load_config\n\n\ndef test_json(tmpdir):\n    conf = tmpdir.join(\"conf.json\")\n    conf.write('{\"a\": \"b\", \"c\": 3}')\n    assert _load_config(str(conf)) == {\"a\": \"b\", \"c\": 3}\n\n\ndef test_load_toml(tmpdir):\n    if sys.version_info < (3, 11):\n        pytest.importorskip(\"tomlkit\")\n    conf = tmpdir.join(\"conf.toml\")\n    conf.write('a = \"b\"\\nc = 3')\n    assert _load_config(str(conf)) == {\"a\": \"b\", \"c\": 3}\n\n\ndef test_load_empty_toml(tmpdir):\n    if sys.version_info < (3, 11):\n        pytest.importorskip(\"tomlkit\")\n    conf = tmpdir.join(\"conf.toml\")\n    conf.write(\"\")\n    assert _load_config(str(conf)) == {}\n\n\ndef test_yaml(tmpdir):\n    pytest.importorskip(\"ruamel.yaml\")\n    conf = tmpdir.join(\"conf.yaml\")\n    conf.write(\"a: b\\nc: 3\")\n    assert _load_config(str(conf)) == {\"a\": \"b\", \"c\": 3}\n\n\ndef test_load_empty_yaml(tmpdir):\n    pytest.importorskip(\"ruamel.yaml\")\n    conf = tmpdir.join(\"conf.yaml\")\n    conf.write(\"\")\n    assert _load_config(str(conf)) == {}\n\n\ndef test_missing(tmpdir):\n    conf = tmpdir.join(\"test.yml\")\n    assert _find_file(str(conf), require=False) is None\n\n\ndef test_missing_strict(tmpdir):\n    conf = tmpdir.join(\"test.yml\")\n    with pytest.raises(FileNotFoundError):\n        _find_file(str(conf))\n\n\ndef test_abspath(tmpdir):\n    conf = tmpdir.join(\"test.yml\")\n    conf.write(\"\")\n    path = _find_file(str(conf))\n    assert path == str(conf)\n\n\ndef test_relative(tmpdir):\n    conf = tmpdir.join(\"test.yml\")\n    conf.write(\"\")\n    os.chdir(conf.dirname)\n    assert _find_file(\"test.yml\") == str(conf)\n"
  },
  {
    "path": "tests/test_files.py",
    "content": "import json\n\nfrom goodconf import GoodConf\n\nfrom .utils import env_var\n\n\ndef test_conf_env_var(mocker, tmpdir):\n    mocked_load_config = mocker.patch(\"goodconf._load_config\")\n    path = tmpdir.join(\"myapp.json\")\n    path.write(\"\")\n\n    class G(GoodConf):\n        model_config = {\"file_env_var\": \"CONF\"}\n\n    with env_var(\"CONF\", str(path)):\n        g = G()\n        g.load()\n    mocked_load_config.assert_called_once_with(str(path))\n\n\ndef test_conflict(tmpdir):\n    path = tmpdir.join(\"myapp.json\")\n    path.write(json.dumps({\"A\": 1, \"B\": 2}))\n\n    class G(GoodConf):\n        A: int\n        B: int\n\n        model_config = {\"default_files\": [path]}\n\n    with env_var(\"A\", \"3\"):\n        g = G()\n        g.load()\n    assert g.A == 3\n    assert g.B == 2\n\n\ndef test_all_env_vars(mocker):\n    mocked_set_values = mocker.patch(\"goodconf.BaseSettings.__init__\")\n    mocked_load_config = mocker.patch(\"goodconf._load_config\")\n\n    class G(GoodConf):\n        pass\n\n    g = G()\n    g.load()\n    mocked_set_values.assert_called_once_with()\n    mocked_load_config.assert_not_called()\n\n\ndef test_provided_file(mocker, tmpdir):\n    mocked_load_config = mocker.patch(\"goodconf._load_config\")\n    path = tmpdir.join(\"myapp.json\")\n    path.write(\"\")\n\n    class G(GoodConf):\n        pass\n\n    g = G()\n    g.load(str(path))\n    mocked_load_config.assert_called_once_with(str(path))\n\n\ndef test_provided_file_from_init(mocker, tmpdir):\n    mocked_load_config = mocker.patch(\"goodconf._load_config\")\n    path = tmpdir.join(\"myapp.json\")\n    path.write(\"\")\n\n    class G(GoodConf):\n        pass\n\n    g = G(config_file=str(path))\n    g.load()\n    mocked_load_config.assert_called_once_with(str(path))\n\n\ndef test_default_files(mocker, tmpdir):\n    mocked_load_config = mocker.patch(\"goodconf._load_config\")\n    path = tmpdir.join(\"myapp.json\")\n    path.write(\"\")\n    bad_path = tmpdir.join(\"does-not-exist.json\")\n\n    class G(GoodConf):\n        model_config = {\"default_files\": [str(bad_path), str(path)]}\n\n    g = G()\n    g.load()\n    mocked_load_config.assert_called_once_with(str(path))\n"
  },
  {
    "path": "tests/test_goodconf.py",
    "content": "import json\nimport os\nimport re\nfrom textwrap import dedent\nfrom typing import Literal\n\nimport pytest\nfrom pydantic import ValidationError\nfrom pydantic.fields import FieldInfo\n\nfrom goodconf import Field, FileConfigSettingsSource, GoodConf\nfrom tests.utils import env_var\n\n\ndef test_initial():\n    class TestConf(GoodConf):\n        a: bool = Field(initial=lambda: True)\n        b: bool = Field(default=False)\n\n    initial = TestConf.get_initial()\n    assert len(initial) == 2\n    assert initial[\"a\"] is True\n    assert initial[\"b\"] is False\n\n\ndef test_dump_json():\n    class TestConf(GoodConf):\n        a: bool = Field(initial=lambda: True)\n\n    assert TestConf.generate_json() == '{\\n  \"a\": true\\n}'\n    assert TestConf.generate_json(not_a_value=True) == '{\\n  \"a\": true\\n}'\n    assert TestConf.generate_json(a=False) == '{\\n  \"a\": false\\n}'\n\n\ndef test_dump_toml():\n    pytest.importorskip(\"tomlkit\")\n\n    class TestConf(GoodConf):\n        a: bool = False\n        b: str = \"Happy\"\n\n    output = TestConf.generate_toml()\n    assert \"a = false\" in output\n    assert 'b = \"Happy\"' in output\n\n    class TestConf(GoodConf):\n        \"Configuration for My App\"\n\n        a: str = Field(description=\"this is a\")\n        b: str\n\n    output = TestConf.generate_toml()\n\n    assert \"# Configuration for My App\\n\" in output\n    assert 'a = \"\" # this is a' in output\n    assert 'b = \"\"' in output\n\n\ndef test_dump_yaml():\n    pytest.importorskip(\"ruamel.yaml\")\n\n    class TestConf(GoodConf):\n        \"Configuration for My App\"\n\n        a: str = Field(description=\"this is a\")\n        b: str\n\n    output = TestConf.generate_yaml()\n    output = re.sub(r\" +\\n\", \"\\n\", output)\n    assert \"\\n# Configuration for My App\\n\" in output\n    assert (\n        dedent(\n            \"\"\"\\\n        # this is a\n        a: ''\n        \"\"\"\n        )\n        in output\n    )\n    assert \"b: ''\" in output\n\n    output_override = TestConf.generate_yaml(b=\"yes\")\n    assert \"a: ''\" in output_override\n    assert \"b: yes\" in output_override\n\n\ndef test_dump_yaml_no_docstring():\n    pytest.importorskip(\"ruamel.yaml\")\n\n    class TestConf(GoodConf):\n        a: str = Field(description=\"this is a\")\n\n    output = TestConf.generate_yaml()\n    output = re.sub(r\" +\\n\", \"\\n\", output)\n    assert output == dedent(\n        \"\"\"\n        # this is a\n        a: ''\n        \"\"\"\n    )\n\n\ndef test_dump_yaml_none():\n    pytest.importorskip(\"ruamel.yaml\")\n\n    class TestConf(GoodConf):\n        a: str | None\n\n    output = TestConf.generate_yaml()\n    assert output.strip() == \"a: ~\"\n\n\ndef test_generate_markdown():\n    help_ = \"this is a\"\n\n    class TestConf(GoodConf):\n        \"Configuration for My App\"\n\n        a: int = Field(description=help_, default=None)\n        b: int = Field(description=help_, default=5)\n        c: str\n\n    mkdn = TestConf.generate_markdown()\n    # Not sure on final format, just do some basic smoke tests\n    assert TestConf.__doc__ in mkdn\n    assert help_ in mkdn\n\n\ndef test_generate_markdown_no_docstring():\n    help_ = \"this is a\"\n\n    class TestConf(GoodConf):\n        a: int = Field(description=help_, default=5)\n        b: str\n\n    mkdn = TestConf.generate_markdown()\n    # Not sure on final format, just do some basic smoke tests\n    assert f\"  * description: {help_}\" in mkdn.splitlines()\n\n\ndef test_generate_markdown_default_false():\n    class TestConf(GoodConf):\n        a: bool = Field(default=False)\n\n    lines = TestConf.generate_markdown().splitlines()\n    assert \"  * type: `bool`\" in lines\n    assert \"  * default: `False`\" in lines\n\n\ndef test_generate_markdown_types():\n    class TestConf(GoodConf):\n        a: Literal[\"a\", \"b\"] = Field(default=\"a\")\n        b: list[str] = Field()\n        c: None\n\n    lines = TestConf.generate_markdown().splitlines()\n    assert \"  * type: `Literal['a', 'b']`\" in lines\n    assert \"  * type: `list[str]`\" in lines\n    assert \"default: `PydanticUndefined`\" not in str(lines)\n\n\ndef test_generate_markdown_required():\n    class TestConf(GoodConf):\n        a: str\n\n    lines = TestConf.generate_markdown().splitlines()\n    assert \"* **a** _REQUIRED_\" in lines\n\n\ndef test_undefined():\n    c = GoodConf()\n    with pytest.raises(AttributeError):\n        c.UNDEFINED  # noqa: B018\n\n\ndef test_required_missing():\n    class TestConf(GoodConf):\n        a: str = Field()\n\n    c = TestConf()\n\n    with pytest.raises(ValidationError):\n        c.load()\n\n    with pytest.raises(ValidationError):\n        TestConf(load=True)\n\n\ndef test_default_values_are_used(monkeypatch):\n    \"\"\"\n    Covers regression in: https://github.com/lincolnloop/goodconf/pull/51\n\n    Requires more than one defined field to reproduce.\n    \"\"\"\n    monkeypatch.delenv(\"a\", raising=False)\n    monkeypatch.setenv(\"b\", \"value_from_env\")\n    monkeypatch.delenv(\"c\", raising=False)\n\n    class TestConf(GoodConf):\n        a: str = Field(default=\"default_for_a\")\n        b: str = Field(initial=lambda: \"1234\")\n        c: str = Field(\"default_for_c\")\n\n    c = TestConf()\n    c.load()\n\n    assert c.a == \"default_for_a\"\n    assert c.b == \"value_from_env\"\n    assert c.c == \"default_for_c\"\n\n\ndef test_set_on_init():\n    class TestConf(GoodConf):\n        a: str = Field()\n\n    val = \"test\"\n    c = TestConf(a=val)\n    assert c.a == val\n\n\ndef test_env_prefix():\n    class TestConf(GoodConf):\n        a: bool = False\n\n        model_config = {\"env_prefix\": \"PREFIX_\"}\n\n    with env_var(\"PREFIX_A\", \"True\"):\n        c = TestConf(load=True)\n\n    assert c.a\n\n\ndef test_precedence(tmpdir):\n    path = tmpdir.join(\"myapp.json\")\n    path.write(json.dumps({\"init\": \"file\", \"env\": \"file\", \"file\": \"file\"}))\n\n    class TestConf(GoodConf, default_files=[path]):\n        init: str = \"\"\n        env: str = \"\"\n        file: str = \"\"\n\n    os.environ[\"INIT\"] = \"env\"\n    os.environ[\"ENV\"] = \"env\"\n    try:\n        c = TestConf(init=\"init\")\n        assert c.init == \"init\"\n        assert c.env == \"env\"\n        assert c.file == \"file\"\n    finally:\n        del os.environ[\"INIT\"]\n        del os.environ[\"ENV\"]\n\n\ndef test_fileconfigsettingssource_repr():\n    class SettingsClass:\n        model_config = {}\n\n    fileconfigsettingssource = FileConfigSettingsSource(SettingsClass)\n\n    assert repr(fileconfigsettingssource) == \"FileConfigSettingsSource()\"\n\n\ndef test_fileconfigsettingssource_get_field_value():\n    class SettingsClass:\n        model_config = {}\n\n    fileconfigsettingssource = FileConfigSettingsSource(SettingsClass)\n    field = FieldInfo(title=\"testfield\")\n    assert fileconfigsettingssource.get_field_value(field, \"testfield\") == (\n        None,\n        \"\",\n        False,\n    )\n    assert fileconfigsettingssource.get_field_value(None, \"a\") == (None, \"\", False)\n"
  },
  {
    "path": "tests/test_initial.py",
    "content": "import pytest\n\nfrom goodconf import Field, GoodConf, initial_for_field\n\nfrom .utils import KEY\n\n\ndef test_initial():\n    class C(GoodConf):\n        f: str = Field(initial=lambda: \"x\")\n\n    assert initial_for_field(KEY, C.model_fields[\"f\"]) == \"x\"\n\n\ndef test_initial_bad():\n    class C(GoodConf):\n        f: str = Field(initial=\"x\")\n\n    with pytest.raises(ValueError, match=\"callable\"):\n        initial_for_field(KEY, C.model_fields[\"f\"])\n\n\ndef test_initial_default():\n    class C(GoodConf):\n        f: str = Field(\"x\")\n\n    assert initial_for_field(KEY, C.model_fields[\"f\"]) == \"x\"\n\n\ndef test_initial_default_factory():\n    class C(GoodConf):\n        f: str = Field(default_factory=lambda: \"y\")\n\n    assert initial_for_field(KEY, C.model_fields[\"f\"]) == \"y\"\n\n\ndef test_no_initial():\n    class C(GoodConf):\n        f: str = Field()\n\n    assert initial_for_field(KEY, C.model_fields[\"f\"]) == \"\"\n\n\ndef test_default_initial():\n    \"\"\"Can get initial when Field is not used\"\"\"\n\n    class G(GoodConf):\n        a: str = \"test\"\n\n    initial = G().get_initial()\n    assert initial[\"a\"] == \"test\"\n\n\ndef test_optional_initial():\n    class G(GoodConf):\n        a: str | None\n\n    initial = G().get_initial()\n    assert initial[\"a\"] is None\n"
  },
  {
    "path": "tests/utils.py",
    "content": "import os\nfrom contextlib import contextmanager\n\nKEY = \"GOODCONF_TEST\"\n\n\n@contextmanager\ndef env_var(key, value):\n    os.environ[key] = value\n    try:\n        yield\n    finally:\n        del os.environ[key]\n"
  }
]