Repository: willkg/everett Branch: main Commit: 7cf9c37bced7 Files: 58 Total size: 245.0 KB Directory structure: gitextract_fnd3ql0f/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── main.yml ├── .gitignore ├── .readthedocs.yaml ├── CODEOWNERS ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs/ │ ├── Makefile │ ├── api.rst │ ├── components.rst │ ├── conf.py │ ├── configmanager.rst │ ├── configuration.rst │ ├── dev.rst │ ├── documenting.rst │ ├── environments.rst │ ├── history.rst │ ├── index.rst │ ├── parsers.rst │ ├── recipes.rst │ ├── test_code.py │ └── testing.rst ├── examples/ │ ├── component_appconfig.py │ ├── componentapp.py │ ├── components_subclass.py │ ├── environments.py │ ├── handling_exceptions.py │ ├── msg_builder.py │ ├── myserver.py │ ├── myserver_with_environments.py │ ├── namespaces.py │ ├── namespaces2.py │ ├── parser_examples.py │ ├── recipes_alternate_keys.py │ ├── recipes_appconfig.py │ ├── recipes_djangosettings.py │ ├── recipes_shared.py │ └── testdebug.py ├── justfile ├── pyproject.toml ├── src/ │ └── everett/ │ ├── __init__.py │ ├── ext/ │ │ ├── __init__.py │ │ ├── inifile.py │ │ └── yamlfile.py │ ├── manager.py │ └── sphinxext.py └── tests/ ├── basic_component_config.py ├── basic_module_config.py ├── conftest.py ├── data/ │ ├── config_test.ini │ └── config_test_original.ini ├── ext/ │ ├── test_inifile.py │ └── test_yamlfile.py ├── simple_module_config.py ├── test_manager.py └── test_sphinxext.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ --- version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" rebase-strategy: "disabled" ================================================ FILE: .github/workflows/main.yml ================================================ --- name: CI on: push: branches: - 'main' pull_request: branches: - 'main' jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] name: Python ${{ matrix.python-version}} steps: - uses: actions/checkout@v5.0.0 - name: Set up Python uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} - name: Update pip and install dev requirements run: | python -m pip install --upgrade pip pip install '.[dev]' - name: Test run: tox ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg uv.lock # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx-generated things docs/_build/ # PyBuilder target/ .vscode/ ================================================ FILE: .readthedocs.yaml ================================================ --- # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-24.04 tools: python: "3.10" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Install dev requirements so that the documentation can build correctly python: install: - method: pip path: . extra_requirements: - dev - ini - sphinx - yaml ================================================ FILE: CODEOWNERS ================================================ * @willkg ================================================ FILE: HISTORY.rst ================================================ History ======= 3.5.0 (October 15th, 2025) -------------------------- Backwards incompatibel changes: * Drop support for Python 3.9. (#282) * Deprecate Everett. I encourage you to switch from Everett to pydantic-settings. See https://github.com/willkg/everett/issues/278 Fixes and features: * Add support for Python 3.14. (#283) 3.4.0 (October 30th, 2024) -------------------------- Backwards incompatible changes: * Drop support for Python 3.8. Thanks, Rob! Fixes and features: * Add support for Python 3.13. (#260) Thanks, Rob! * Add support for underscore as first character in variable names in env files. (#263) * Add ``ChoiceOf`` parser for enforcing configuration values belong in specified value domain. (#253) * Fix ``autocomponentconfig`` to support components with no options. (#244) * Add ``allow_empty`` option to ``ListOf`` parser that lets you specify whether empty strings are a configuration error or not. (#268) 3.3.0 (November 6th, 2023) -------------------------- Backwards incompatible changes: * Drop support for Python 3.7. (#220) Fixes and features: * Add support for Python 3.12 (#221) * Fix env file parsing in regards to quotes. (#230) 3.2.0 (March 21st, 2023) ------------------------ Fixes and features: * Implement ``default_if_empty`` argument which will return the default value (if specified) if the value is the empty string. (#205) * Implement ``parse_time_period`` parser for converting time periods like "10m4s" into the total number of seconds that represents. :: >>> from everett.manager import parse_time_period >>> parse_time_period("4m") 240 (#203) * Implement ``parse_data_size`` parser for converting values like "40gb" into the total number of bytes that represents. :: >>> from everett.manager import parse_data_size >>> parse_time_period("40gb") 40000000000 (#204) * Fix an ``UnboundLocalError`` when using ``automoduleconfig`` and providing a Python dotted path to a thing that either kicks up an ``ImportError`` or doesn't exist. Now it raises a more helpful error. (#201) 3.1.0 (October 26th, 2022) -------------------------- Fixes and features: * Add support for Python 3.11. (#187) * Add ``raise_configuration_error`` method on ``ConfigManager``. (#185) * Improve ``automoduleconfig`` to walk the whole AST and document configuration set by assign:: SOMEVAR = _config("somevar") and dict:: SOMEGROUP = { "SOMEVAR": _config("somevar"), } (#184) * Fix options not showing up on ReadTheDocs. (#186) 3.0.0 (January 13th, 2022) -------------------------- Backwards incompatible changes: * Dropped support for Python 3.6. (#176) * Dropped ``autocomponent`` Sphinx directive in favor of ``autocomponentconfig``. Fixes and features: * Add support for Python 3.10. (#173) * Rework namespaces so that you can apply a namespace (``with_namespace()``) after binding a component (``with_options()``) (#175) * Overhauled, simplified, and improved documentation. Files with example output are now generated using `cog `_. * Rewrite Sphinx extension. This now supports manually documenting configuration using ``everett:component`` and ``everett:option`` directives. This adds ``:everett:component:`` and ``:everett:option:`` roles for linking to specific configuration in the docs. It also addsh ``autocomponentconfig`` and ``automoduleconfig`` directives for automatically generating documentation. When using these directives, items are added to the index and everything is linkable making it easier to find and talk to users about specific configuration items. (#172) 2.0.1 (August, 23rd, 2021) -------------------------- Fixes: * Fix Sphinx warning about roles in Everett sphinxext. (#165) * Fix ``get_runtime_config`` to work with slots (#166) 2.0.0 (July 27th, 2021) ----------------------- Backwards incompatible changes: * This radically reduces the boilerplate required to define components. It also improves the connections between things so it's easier to: * determine the configuration required for a single component (taking into account superclasses, overriding, etc) * determine the runtime configuration for a component tree given a configuration manager Previously, components needed to subclass RequiredConfigMixin and provide a "required_config" class attribute. Something like this:: from everett.component import RequiredConfigMixin, ConfigOptions class SomeClass(RequiredConfigMixin): required_config = ConfigOptions() required_config.add_option( "some_option", default="42", ) That's been slimmed down and now looks like this:: from everett.manager import Option class SomeClass: class Config: some_option = Option(default="42") That's much simpler and the underlying implementation code is less tangled and complex, too. If you used ``everett.component.RequiredConfigMixin`` or ``everett.component.ConfigOptions``, you'll need to update your classes. If you didn't use those things, then you don't have to make any changes. See the documentation on components for how it all works now. * Changed the way configuration variables are referred to in configuration error messages. Previously, I tried to use a general way "namespace=something key=somethingelse" but that's confusing and won't match up with project documentation. I changed it to the convention used in the process environment and env files. For example, ``FOO_BAR``. If you use INI or YAML for configuration, you can specify a ``msg_builder`` argument when you build the ``ConfigManager`` and build error messages tailored to your users. Fixes: * Switch to ``src/`` repository layout. * Added type annotations and type checking during CI. (#155) * Standardized on f-strings across the codebase. * Switched Sphinx theme. * Update of documentation, fleshed out and simplified examples, cleaned up language, reworked structure of API section (previously called Library or some unhelpful thing like that), etc. 1.0.3 (October 28th, 2020) -------------------------- Backwards incompatible changes: * Dropped support for Python 3.4. (#96) * Dropped support for Python 3.5. (#116) Fixes: * Add support for Python 3.7. (#68) * Add support for Python 3.8. (#102) * Add support for Python 3.9. (#117) * Reformatted code with Black, added Makefile, switched to GitHub Actions. * Fix ``get_runtime_config()`` to infer namespaces. (#118) * Fix ``RemovedInSphinx50Warning``. (#115) * Documentation fixes and clarifications. 1.0.2 (February 22nd, 2019) --------------------------- Fixes: * Improve documentation. * Fix problems when there are nested ``BoundConfigs``. Now they work correctly. (#90) * Add "meta" to options letting you declare additional data on the option when you're adding it. For example, this lets you do things like mark options as "secrets" so that you know which ones to ``******`` out when logging your configuration. (#88) 1.0.1 (January 8th, 2019) ------------------------- Fixes: * Fix documentation issues. * Package missing ``everett.ext``. Thank you, dsblank! (#84) 1.0.0 (January 7th, 2019) ------------------------- Backwards incompatible changes: * Dropped support for Python 2.7. Everett no longer supports Python 2. (#73) * Dropped support for Python 3.3 and added support for Python 3.7. Thank you, pjz! (#68) * Moved ``ConfigIniEnv`` to a different module. Now you need to import it like this:: from everett.ext.inifile import ConfigIniEnv (#79) Features: * Everett now logs configuration discovery in the ``everett`` logger at the ``logging.DEBUG`` level. This is helpful for trouble-shooting some kinds of issues. (#74) * Everett now has a YAML configuration environment. In order to use it, you need to install its requirements:: $ pip install everett[yaml] Then you can import it like this:: from everett.ext.yamlfile import ConfigYamlEnv (#72) Fixes: * Everett no longer requires ``configobj``--it's now optional. If you use ``ConfigIniEnv``, you can install it with:: $ pip install everett[ini] (#79) * Fixed list parsing and file discovery in ConfigIniEnv so they match the docs and are more consistent with other envs. Thank you, apollo13! (#71) * Added a ``.basic_config()`` for fast opinionated setup that uses the process environment and a ``.env`` file in the current working directory. * Switching to semver. 0.9 (April 7th, 2017) --------------------- Changed: * Rewrite Sphinx extension. The extension is now in the ``everett.sphinxext`` module and the directive is now ``.. autocomponent::``. It generates better documentation and it now indexes Everett components and options. This is backwards-incompatible. You will need to update your Sphinx configuration and documentation. * Changed the ``HISTORY.rst`` structure. * Changed the repr for ``everett.NO_VALUE`` to ``"NO_VALUE"``. * ``InvalidValueError`` and ``ConfigurationMissingError`` now have ``namespace``, ``key``, and ``parser`` attributes allowing you to build your own messages. Fixed: * Fix an example in the docs where the final key was backwards. Thank you, pjz! Documentation fixes and updates. 0.8 (January 24th, 2017) ------------------------ Added: * Add ``:namespace:`` and ``:case:`` arguments to autoconfig directive. These make it easier to cater your documentation to your project's needs. * Add support for Python 3.6. Minor documentation fixes and updates. 0.7 (January 5th, 2017) ----------------------- Added: * Feature: You can now include documentation hints and urls for ``ConfigManager`` objects and config options. This will make it easier for your users to debug configuration errors they're having with your software. Fixed: * Fix ``ListOf`` so it returns empty lists rather than a list with a single empty string. Documentation fixes and updates. 0.6 (November 28th, 2016) ------------------------- Added: * Add ``RequiredConfigMixin.get_runtime_config()`` which returns the runtime configuration for a component or tree of components. This lets you print runtime configuration at startup, generate INI files, etc. * Add ``ConfigObjEnv`` which lets you use an object for configuration. This works with argparse's Namespace amongst other things. Changed: * Change ``:show-docstring:`` to take an optional value which is the attribute to pull docstring content from. This means you don't have to mix programming documentation with user documentation--they can be in different attributes. * Improve configuration-related exceptions. With Python 3, configuration errors all derive from ``ConfigurationError`` and have helpful error messages that should make it clear what's wrong with the configuration value. With Python 2, you can get other kinds of Exceptions thrown depending on the parser used, but configuration error messages should still be helpful. Documentation fixes and updates. 0.5 (November 8th, 2016) ------------------------ Added: * Add ``:show-docstring:`` flag to ``autoconfig`` directive. * Add ``:hide-classname:`` flag to ``autoconfig`` directive. Changed: * Rewrite ``ConfigIniEnv`` to use configobj which allows for nested sections in INI files. This also allows you to specify multiple INI files and have later ones override earlier ones. Fixed: * Fix ``autoconfig`` Sphinx directive and add tests--it was all kinds of broken. Documentation fixes and updates. 0.4 (October 27th, 2016) ------------------------ Added: * Add ``raw_value`` argument to config calls. This makes it easier to write code that prints configuration. Fixed: * Fix ``listify(None)`` to return ``[]``. Documentation fixes and updates. 0.3.1 (October 12th, 2016) -------------------------- Fixed: * Fix ``alternate_keys`` with components. Previously it worked for everything but components. Now it works with components, too. Documentation fixes and updates. 0.3 (October 6th, 2016) ----------------------- Added: * Add ``ConfigManager.from_dict()`` shorthand for building configuration instances. * Add ``.get_namespace()`` to ``ConfigManager`` and friends for getting the complete namespace for a given config instance as a list of strings. * Add ``alternate_keys`` to config call. This lets you specify a list of keys in order to try if the primary key doesn't find a value. This is helpful for deprecating keys that you used to use in a backwards-compatible way. * Add ``root:`` prefix to keys allowing you to look outside of the current namespace and at the configuration root for configuration values. Changed: * Make ``ConfigDictEnv`` case-insensitive to keys and namespaces. Documentation fixes and updates. 0.2 (August 16th, 2016) ----------------------- Added: * Add ``ConfigEnvFileEnv`` for supporting ``.env`` files. Thank you, Paul! * Add "on" and "off" as valid boolean values. This makes it easier to use config for feature flippers. Thank you, Paul! Changed: * Change ``ConfigIniEnv`` to take a single path or list of paths. Thank you, Paul! * Make ``NO_VALUE`` falsy. Fixed: * Fix ``__call__`` returning None--it should return ``NO_VALUE``. Lots of docs updates: finished the section about making your own parsers, added a section on using dj-database-url, added a section on django-cache-url and expanded on existing examples. 0.1 (August 1st, 2016) ---------------------- Initial writing. ================================================ FILE: LICENSE ================================================ Mozilla Public License, version 2.0 1. Definitions 1.1. “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. “Contributor Version” means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. 1.3. “Contribution” means Covered Software of a particular Contributor. 1.4. “Covered Software” means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. “Incompatible With Secondary Licenses” means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. “Executable Form” means any form of the work other than Source Code Form. 1.7. “Larger Work” means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. “License” means this document. 1.9. “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. “Modifications” means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. “Patent Claims” of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. “Secondary License” means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. “Source Code Form” means the form of the work preferred for making modifications. 1.14. “You” (or “Your”) means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - “Incompatible With Secondary Licenses” Notice This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: MANIFEST.in ================================================ include *.rst include pyproject.toml include justfile include LICENSE include .readthedocs.yaml recursive-include docs *.rst recursive-include docs *.py recursive-include docs Makefile recursive-include docs_tmpl *.rst recursive-include examples *.py recursive-include tests *.env recursive-include tests *.ini recursive-include tests *.py ================================================ FILE: README.rst ================================================ .. NOTE: Make sure to edit the template for this file in docs_tmpl/ and .. not the cog-generated version. ======= Everett ======= **Status 2025-10-15: This project is deprecated.** Everett is a Python configuration library for your app. :Code: https://github.com/willkg/everett :Issues: https://github.com/willkg/everett/issues :License: MPL v2 :Documentation: https://everett.readthedocs.io/ Goals ===== Goals of Everett: 1. flexible configuration from multiple configured environments 2. easy testing with configuration 3. easy automated documentation of configuration for users From that, Everett has the following features: * is flexible for your configuration environment needs and supports process environment, env files, dicts, INI files, YAML files, and writing your own configuration environments * facilitates helpful error messages for users trying to configure your software * has a Sphinx extension for documenting configuration including ``autocomponentconfig`` and ``automoduleconfig`` directives for automatically generating configuration documentation * facilitates testing of configuration values * supports parsing values of a variety of types like bool, int, lists of things, classes, and others and lets you write your own parsers * supports key namespaces * supports component architectures * works with whatever you're writing--command line tools, web sites, system daemons, etc Everett is inspired by `python-decouple `__ and `configman `__. Install ======= Run:: $ pip install everett Some configuration environments require additional dependencies:: # For INI support $ pip install 'everett[ini]' # for YAML support $ pip install 'everett[yaml]' Quick start =========== Example: .. [[[cog import cog with open("examples/myserver.py", "r") as fp: cog.outl("\n::\n") for line in fp.readlines(): if line.strip(): cog.out(f" {line}") else: cog.outl() cog.outl() ]]] :: # myserver.py """ Minimal example showing how to use configuration for a web app. """ from everett.manager import ConfigManager config = ConfigManager.basic_config( doc="Check https://example.com/configuration for documentation." ) host = config("host", default="localhost") port = config("port", default="8000", parser=int) debug_mode = config( "debug", default="False", parser=bool, doc="Set to True for debugmode; False for regular mode", ) print(f"host: {host}") print(f"port: {port}") print(f"debug_mode: {debug_mode}") .. [[[end]]] Then you can run it: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") ret = subprocess.run(["python", "examples/myserver.py"], capture_output=True) cog.outl("\n::\n") cog.outl(" $ python myserver.py") for line in ret.stdout.decode("utf-8").splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ python myserver.py host: localhost port: 8000 debug_mode: False .. [[[end]]] You can set environment variables to affect configuration: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") os.environ["PORT"] = "7000" cog.outl("\n::\n") cog.outl(" $ PORT=7000 python myserver.py") ret = subprocess.run(["python", "examples/myserver.py"], capture_output=True) for line in ret.stdout.decode("utf-8").splitlines(): cog.outl(f" {line}") cog.outl() del os.environ["PORT"] ]]] :: $ PORT=7000 python myserver.py host: localhost port: 7000 debug_mode: False .. [[[end]]] It checks a ``.env`` file in the current directory: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") with open(".env", "w") as fp: fp.write("HOST=127.0.0.1") cog.outl("\n::\n") cog.outl(" $ echo \"HOST=127.0.0.1\" > .env") cog.outl(" $ python myserver.py") ret = subprocess.run(["python", "examples/myserver.py"], capture_output=True) for line in ret.stdout.decode("utf-8").splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ echo "HOST=127.0.0.1" > .env $ python myserver.py host: 127.0.0.1 port: 8000 debug_mode: False .. [[[end]]] It spits out useful error information if configuration is wrong: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") os.environ["DEBUG"] = "foo" ret = subprocess.run(["python", "examples/myserver.py"], capture_output=True) stderr = ret.stderr.decode("utf-8").strip() stderr = stderr[stderr.find("everett.InvalidValueError"):] cog.outl("\n::\n") cog.outl(" $ DEBUG=foo python myserver.py") cog.outl(" ") for line in stderr.splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ DEBUG=foo python myserver.py everett.InvalidValueError: ValueError: 'foo' is not a valid bool value DEBUG requires a value parseable by everett.manager.parse_bool DEBUG docs: Set to True for debugmode; False for regular mode Project docs: Check https://example.com/configuration for documentation. .. [[[end]]] You can test your code using ``config_override`` in your tests to test various configuration values: .. [[[cog import cog cog.outl("\n::\n") with open("examples/testdebug.py", "r") as fp: for line in fp.readlines(): cog.out(f" {line}") cog.outl() ]]] :: # testdebug.py """ Minimal example showing how to override configuration values when testing. """ import unittest from everett.manager import ConfigManager, config_override class App: def __init__(self): config = ConfigManager.basic_config() self.debug = config("debug", default="False", parser=bool) class TestDebug(unittest.TestCase): def test_debug_on(self): with config_override(DEBUG="on"): app = App() self.assertTrue(app.debug) def test_debug_off(self): with config_override(DEBUG="off"): app = App() self.assertFalse(app.debug) if __name__ == "__main__": unittest.main() .. [[[end]]] Run that: .. [[[cog import cog import os import subprocess ret = subprocess.run(["python", "examples/testdebug.py"], capture_output=True) stderr = ret.stderr.decode("utf-8").strip() cog.outl("\n::\n") cog.outl(" $ python testdebug.py") for line in stderr.splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ python testdebug.py .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK .. [[[end]]] That's perfectly fine for a `12-Factor `__ app. When you outgrow that or need different variations of it, you can switch to creating a ``ConfigManager`` instance that meets your needs. Why not other libs? =================== Most other libraries I looked at had one or more of the following issues: * were tied to a specific web app framework * didn't allow you to specify configuration sources * provided poor error messages when users configure things wrong * had a global configuration object * made it really hard to override specific configuration when writing tests * had no facilities for autogenerating configuration documentation ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = Everett SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile view # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile which $(SPHINXBUILD) @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) view: gnome-open _build/html/index.html ================================================ FILE: docs/api.rst ================================================ === API === This is the API of functions and classes in Everett. Configuration things: * :py:class:`everett.manager.ConfigManager` * :py:class:`everett.manager.Option` Utility functions: * :py:class:`everett.manager.get_config_for_class` * :py:class:`everett.manager.get_runtime_config` Testing utility functions: * :py:class:`everett.manager.config_override` Configuration environments: * :py:class:`everett.manager.ConfigObjEnv` * :py:class:`everett.manager.ConfigDictEnv` * :py:class:`everett.manager.ConfigEnvFileEnv` * :py:class:`everett.manager.ConfigOSEnv` * (INI) :py:class:`everett.ext.inifile.ConfigIniEnv` * (YAML) :py:class:`everett.ext.yamlfile.ConfigYamlEnv` Errors: * :py:class:`everett.ConfigurationError` * :py:class:`everett.InvalidKeyError` * :py:class:`everett.ConfigurationMissingError` * :py:class:`everett.InvalidValueError` Parsers: * :py:func:`everett.manager.parse_bool` * :py:func:`everett.manager.parse_class` * :py:func:`everett.manager.parse_data_size` * :py:func:`everett.manager.ListOf` everett ======= .. automodule:: everett :members: everett.manager =============== .. automodule:: everett.manager :members: :special-members: __init__, __call__ everett.ext.inifile =================== .. automodule:: everett.ext.inifile :members: everett.ext.yamlfile ==================== .. automodule:: everett.ext.yamlfile :members: ================================================ FILE: docs/components.rst ================================================ .. NOTE: Make sure to edit the template for this file in docs_tmpl/ and .. not the cog-generated version. ========== Components ========== .. contents:: :local: .. versionchanged:: 2.0 This is redone for v2.0.0 and simplified. Configuration components ======================== Everett supports configuration components. There are two big use cases for this: 1. Centralizing configuration specification for your application into a single class. 2. Component architectures. Centralizing configuration -------------------------- Instead of having configuration-related bits defined across your codebase, you can define it in a class. Here's an example with an ``AppConfig``: .. literalinclude:: ../examples/component_appconfig.py :language: python Let's run it with the defaults: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") ret = subprocess.run(["python", "examples/component_appconfig.py"], capture_output=True) cog.outl("\n::\n") cog.outl(" $ python component_appconfig.py") for line in ret.stdout.decode("utf-8").splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ python component_appconfig.py debug: False .. [[[end]]] Now with ``DEBUG=true``: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") os.environ["DEBUG"] = "true" ret = subprocess.run(["python", "examples/component_appconfig.py"], capture_output=True) cog.outl("\n::\n") cog.outl(" $ DEBUG=true python component_appconfig.py") for line in ret.stdout.decode("utf-8").splitlines(): cog.outl(f" {line}") cog.outl() del os.environ["DEBUG"] ]]] :: $ DEBUG=true python component_appconfig.py debug: True .. [[[end]]] Let's run a Python shell and do some other things with it: .. doctest:: >>> import component_appconfig debug: False >>> config = component_appconfig.get_config() >>> config("badkey") Traceback (most recent call last): ... everett.InvalidKeyError: 'badkey' is not a valid key for this component Notice how you can't use configuration keys that aren't specified in the bound component. Centrally defining configuration like this helps in a few ways: 1. You can reduce some bugs that occur as your application evolves over time. Every time you use configuration, the ``ConfigManager`` will enforce that the key is a valid option. 2. Your application configuration is centralized in one place instead of spread out across your code base. 3. You can automatically document your configuration using the ``everett.sphinxext`` Sphinx extension and ``autocomponentconfig`` directive:: .. autocomponentconfig:: path.to.AppConfig Because it's automatically documented, your documentation is always up-to-date. Component architectures ----------------------- Everett configuration supports component architectures. Say your app needs to connect to RabbitMQ. With Everett, you can define the component's configuration needs in the component class. Here's an example: .. literalinclude:: ../examples/componentapp.py :language: python That's not wildly exciting, but if the component was in a library of components, then you can string them together using configuraiton. For example, what if the destination wasn't a single bucket, but rather a set of buckets? :: dest_config = config("pipeline", default="dest", parser=ListOf(str)) dest_buckets = [] for name in dest_config: dest_buckets.append(S3Bucket(s3_config.with_namespace(name))) You can autogenerate configuration documentation for this component in your Sphinx docs by including the ``everett.sphinxext`` Sphinx extension and using the ``autocomponentconfig`` directive:: .. autocomponentconfig:: myapp.S3Bucket Subclassing =========== You can subclass components and override configuration options. For example: .. literalinclude:: ../examples/components_subclass.py :language: python That prints: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") ret = subprocess.run(["python", "examples/components_subclass.py"], capture_output=True) cog.outl("\n::\n") cog.outl(" $ python components_subclass.py") for line in ret.stdout.decode("utf-8").splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ python components_subclass.py foo_from_b bar_from_a .. [[[end]]] Getting configuration information for components ================================================ You can get the configuration options for a component class using :py:func:`everett.manager.get_config_for_class`. This returns a dict of ``configuration key -> (option, class)``. This helps with debugging which option came from which class. .. autofunction:: everett.manager.get_config_for_class :noindex: You can get the runtime configuration for a component or tree of components using :py:func:`everett.manager.get_runtime_config`. This returns a list of ``(namespace, key, value, option, class)`` tuples. The value is the computed runtime value taking into account the environments specified in the ``ConfigManager`` and class hierarchies. It'll traverse any instance attributes that are components with options. .. autofunction:: everett.manager.get_runtime_config :noindex: ================================================ FILE: docs/conf.py ================================================ # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import os import sys cwd = os.getcwd() # Add ../src/ directory so we can pull in Everett things using autodoc project_root = os.path.dirname(cwd) src_root = os.path.join(project_root, "src") sys.path.insert(0, src_root) # Add ../examples/ directory so we can use autocomponentconfig with a recipe sys.path.insert(0, os.path.join(project_root, "examples")) import everett # noqa # -- Project information ----------------------------------------------------- project = "Everett" copyright = "2016-2022, Will Kahn-Greene" author = "Will Kahn-Greene" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "everett.sphinxext", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = everett.__version__ # The full version with the release date. release = everett.__version__ # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" autodoc_typehints = "description" autoclass_content = "both" autodoc_default_options = { "class-doc-from": "both", "member-order": "bysource", "inheireted-members": True, } # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Custom sidebar templates, maps document names to template names. # html_sidebars = { "**": [ "about.html", "navigation.html", "relations.html", "searchbox.html", ] } ================================================ FILE: docs/configmanager.rst ================================================ .. NOTE: Make sure to edit the template for this file in docs_tmpl/ and .. not the cog-generated version. ====================== Configuration Managers ====================== .. contents:: :local: Creating a ConfigManager and specifying environments ==================================================== First, you define a :py:class:`everett.manager.ConfigManager` which specifies the environments you want to pull configuration from. Then you can use the :py:class:`everett.manager.ConfigManager` to look up configuration keys. The :py:class:`everett.manager.ConfigManager` will go through the environments in order to find a value for the configuration key. Once it finds a value, it runs it through the parser and returns the parsed value. There are a few ways to create a :py:class:`everett.manager.ConfigManager`. The easiest is to use :py:func:`everett.manager.ConfigManager.basic_config`. For example: .. literalinclude:: ../examples/myserver.py :language: python That creates a :py:class:`everett.manager.ConfigManager` that looks up configuration keys in these environments: 1. the process environment 2. the specified env file which defaults to ``.env`` That works for most cases. You can create your own :py:class:`everett.manager.ConfigManager` and specify environments specific to your needs. For example: .. literalinclude:: ../examples/myserver_with_environments.py :language: python Specifying configuration documentation ====================================== When building a :py:class:`everett.manager.ConfigManager`, you can specify documentation for configuration. It will get printed when there are configuration errors. This is a great place to put a link to configuration documentation. For example: .. literalinclude:: ../examples/myserver.py :language: python Let's run that: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") ret = subprocess.run(["python", "examples/myserver.py"], capture_output=True) cog.outl("\n::\n") cog.outl(" $ python myserver.py") for line in ret.stdout.decode("utf-8").splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ python myserver.py host: localhost port: 8000 debug_mode: False .. [[[end]]] Let's set ``DEBUG`` wrong and see what it tells us: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") os.environ["DEBUG"] = "foo" ret = subprocess.run(["python", "examples/myserver.py"], capture_output=True) stderr = ret.stderr.decode("utf-8").strip() stderr = stderr[stderr.find("everett.InvalidValueError"):] cog.outl("\n::\n") cog.outl(" $ DEBUG=foo python myserver.py") cog.outl(" ") for line in stderr.splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ DEBUG=foo python myserver.py everett.InvalidValueError: ValueError: 'foo' is not a valid bool value DEBUG requires a value parseable by everett.manager.parse_bool DEBUG docs: Set to True for debugmode; False for regular mode Project docs: Check https://example.com/configuration for documentation. .. [[[end]]] Here, we see the documentation for the ``DEBUG`` option, the documentation from the ``ConfigManager``, and the specific Python exception information. ================================================ FILE: docs/configuration.rst ================================================ .. NOTE: Make sure to edit the template for this file in docs_tmpl/ and .. not the cog-generated version. ============= Configuration ============= .. contents:: :local: Extracting values ================= Once you have a configuration manager set up with sources, you can pull configuration values from it. Configuration must have a key. Everything else is optional. Examples: :: config("password") The key is "password". The value is parsed as a string. There is no default value provided so if "password" isn't provided in any of the configuration sources, then this will raise a :py:class:`everett.ConfigurationError`. This is what you want to do to require that a configuration value exist. :: config("name", raise_error=False) The key is "name". The value is parsed as a string. There is no default value provided and raise_error is set to False, so if this configuration variable isn't set anywhere, the result of this will be ``everett.NO_VALUE``. .. Note:: :py:data:`everett.NO_VALUE` is a falsy value so you can use it in comparative contexts:: debug = config("DEBUG", parser=bool, raise_error=False) if not debug: pass :: config("port", parser=int, default="5432") The key is "port". The value is parsed using int. There is a default provided, so if this configuration variable isn't set in the specified sources, the default will be false. Note that the default value is always a string that's parseable by the parser. :: config("username", namespace="db") The key is "username". The namespace is "db". There's no default, so if there's no "username" in namespace "db" configuration variable set in the sources, this will raise a :py:class:`everett.ConfigurationError`. If you're looking up values in the process environment, then the full key would be ``DB_USERNAME``. :: config("password", namespace="postgres", alternate_keys=["db_password", "root:postgres_password"]) The key is "password". The namespace is "postgres". If there is no key "password" in namespace "postgres", then it looks for "db_password" in namespace "postgres". This makes it possible to deprecate old key names, but still support them. If there is no key "password" or "db_password" in namespace "postgres", then it looks at "postgres_password" in the root namespace. This allows you to have multiple components that share configuration like credentials and hostnames. :: config( "debug", default="false", parser=bool, doc="Set to True for debugmode; False for regular mode", ) You can provide a ``doc`` argument which will give users users who are trying to configure your software a more helpful error message when they hit a configuration error. Example of error message for an option that specifies ``doc`` when trying to set ``DEBUG`` to ``foo``: .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") os.environ["DEBUG"] = "foo" ret = subprocess.run(["python", "examples/myserver.py"], capture_output=True) stderr = ret.stderr.decode("utf-8").strip() stderr = stderr[stderr.find("everett.InvalidValueError"):] cog.outl("\n::\n") cog.outl(" $ python example.py") cog.outl(" ") for line in stderr.splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ python example.py everett.InvalidValueError: ValueError: 'foo' is not a valid bool value DEBUG requires a value parseable by everett.manager.parse_bool DEBUG docs: Set to True for debugmode; False for regular mode Project docs: Check https://example.com/configuration for documentation. .. [[[end]]] That last line comes directly from the ``doc`` argument you provide. .. automethod:: everett.manager.ConfigManager.__call__ :noindex: .. autoclass:: everett.ConfigurationError :noindex: .. autoclass:: everett.InvalidValueError :noindex: .. autoclass:: everett.ConfigurationMissingError :noindex: .. autoclass:: everett.InvalidKeyError :noindex: Namespaces ========== Everett has namespaces for grouping related configuration values. For example, this uses "username", "password", and "port" configuration keys in the "db" namespace: .. literalinclude:: ../examples/namespaces.py :language: python These variables in the environment would be ``DB_USERNAME``, ``DB_PASSWORD`` and ``DB_PORT``. .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") ret = subprocess.run(["python", "examples/namespaces.py"], capture_output=True) cog.outl("\n::\n") cog.outl(" $ python namespaces.py") for line in ret.stdout.decode("utf-8").splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ python namespaces.py Opened database with admin/ou812 on port 5432 .. [[[end]]] This is helpful when you need to create two of the same thing, but using separate configuration. What if we had source and destination databases and needed to have the configuration keys separated? .. literalinclude:: ../examples/namespaces2.py :language: python .. [[[cog import cog import os import subprocess if os.path.exists(".env"): os.remove(".env") ret = subprocess.run(["python", "examples/namespaces2.py"], capture_output=True) cog.outl("\n::\n") cog.outl(" $ python namespaces2.py") for line in ret.stdout.decode("utf-8").splitlines(): cog.outl(f" {line}") cog.outl() ]]] :: $ python namespaces2.py Opened database with admin/ou812 on port 5432 Opened database with admin/P9rwvnnj8CidECMb on port 5432 .. [[[end]]] Handling exceptions when extracting values ========================================== When the namespaced key isn't found in any of the sources, then Everett will raise an exception that is a subclass of :py:class:`everett.ConfigurationError`. This makes it easier to programmatically figure out what happened. If you don't like what Everett prints by default, you can catch the errors and print something different. For example: .. literalinclude:: ../examples/handling_exceptions.py :language: python Also, you can change the structure of the error message by passing in a ``msg_builder`` argument to the :py:class:`everett.manager.ConfigManager`. For example, say your project is entirely done with INI configuration. Then you'd want to tailor the message accordingly. .. literalinclude:: ../examples/msg_builder.py :language: python That prints this: .. [[[cog import cog import os import subprocess os.environ["DEBUG"] = "lizard" if os.path.exists(".env"): os.remove(".env") ret = subprocess.run(["python", "examples/msg_builder.py"], capture_output=True) stderr = ret.stderr.decode("utf-8").strip() stderr = stderr[stderr.find("everett.InvalidValueError"):] cog.outl("\n::\n") cog.outl(" $ DEBUG=lizard python msg_builder.py") cog.outl(" ") for line in stderr.splitlines(): cog.outl(f" {line}") cog.outl() del os.environ["DEBUG"] ]]] :: $ DEBUG=lizard python msg_builder.py everett.InvalidValueError: Dear user. debug in section [main] is not correct. Please fix it. .. [[[end]]] Trouble-shooting and logging what happened ============================================= If you have a non-trivial Everett configuration, it might be difficult to figure out exactly why a key lookup failed. Everett logs to the ``everett`` logger at the ``logging.DEBUG`` level. You can enable this logging and get a clearer idea of what's going on. See `Python logging documentation `_ for details on enabling logging. ================================================ FILE: docs/dev.rst ================================================ ================== Developing Everett ================== Install for development ======================= Requirements: * `uv `__ * `just `__ * `git `__ Clone the repository:: $ git clone https://github.com/willkg/everett Create a dev environment:: $ just devenv Development recipes are in ``justfile``. You can get a list with ``just --list``. ================================================ FILE: docs/documenting.rst ================================================ ========================= Documenting configuration ========================= .. contents:: :local: It's hard to keep configuration documentation up-to-date as projects change over time. Everett comes with a `Sphinx `_ extension for documenting configuration. It has ``autocomponentconfig`` and ``automoduleconfig`` directives for automatically generating documentation. It also has ``everett:component`` and ``everett:option`` directives for manually documenting configuration. It also comes with ``:everett:option:`` and ``:everett:component:`` roles letting you create links to specific configuration things in your documentation. Configuration options are added to the index and have unique links making it easier to find and point people to specific configuration documentation. .. versionchanged:: 3.0.0 Complete rewrite of Sphinx directives. Directives ========== .. rst:directive:: automoduleconfig **Requires Python 3.8 or higher.** Automatically documents the configuration options set in a Python module using the specified :py:class:`everett.manager.ConfigManager`. The argument is the Python dotted path to the :py:class:`everett.manager.ConfigManager` instance. .. Note:: The automoduleconfig directive works by parsing the Python module as an AST and then traverses the AST. It does not execute the module, so it doesn't evaluate any values. .. Note:: ``automoduleconfig`` requires Python 3.8 or higher. If you're using ReadTheDocs, it defaults to Python 3.7. You'll need to configure the version of Python to use by adding a configuration file. See `ReadTheDocs configuration file documentation `_ for more details. .. rubric:: Options .. rst:directive:option:: show-table :type: no value If set, will create a table summarizing the options in this module with links to the option details. .. rst:directive:option:: hide-name :type: no value If set, this will hide the name derived from the Python dotted path and use "Configuration" instead. This affects how the options are indexed. If you're documenting multiple modules this way, options that exist in multiple modules will create a conflict. .. rst:directive:option:: show-docstring :type: str, empty str, or omitted If omitted, this does nothing. If set, but with no value, this will include the module docstring in the documentation. If set with a value of the name of an attribute in the module, this will include the value of that attribute in the documentation. Example to include the module ``__doc__``: :: .. automoduleconfig:: myproject.settings._config :show-docstring: Example to include the value of the value of the ``HELP`` attribute: :: .. automoduleconfig:: myproject.settings._config :show-docstring: HELP .. rst:directive:option:: namespace :type: str If set, this prefixes all the option keys with the specified namespace. For example, if you set namespace to ``source_db``, then key ``host`` would result in ``source_db_host`` being documented. (Case is dependent on the "case" directive option.) .. rst:directive:option:: case :type: "upper", "lower", or omitted Specifies whether to convert the full namespaced key to all uppercase, all lowercase, or leave it as is. .. rst:directive:: autocomponentconfig Automatically documents the configuration options for the specified class and its superclasses. The argument is the Python dotted path to the class. .. Warning:: ``autocomponentconfig`` **imports** the code to be documented. If any of the imported modules have side-effects at import, they will be executed when building the documentation. .. rubric:: Options .. rst:directive:option:: show-table :type: no value If set, will create a table summarizing the options in this component with links to the option details. .. rst:directive:option:: hide-name :type: no value If set, this will hide the name of the class and use "Configuration" instead. This affects how the options are indexed. If you're documenting multiple classes this way, options that exist in multiple classes will create a conflict. .. rst:directive:option:: show-docstring :type: str, empty str, or omitted If omitted, this does nothing. If set, but with no value, this will include the class docstring in the documentation. If set with a value of the name of an attribute of the class, this will include the value of that attribute in the documentation. Example to include the class docstring: :: .. automoduleconfig:: myproject.MyClass :show-docstring: Example to include the value of the value of the ``HELP`` attribute: :: .. automoduleconfig:: myproject.MyClass :show-docstring: HELP .. rst:directive:option:: namespace :type: str If set, this prefixes all the option keys with the specified namespace. For example, if you set namespace to ``source_db``, then key ``host`` would result in ``source_db_host`` being documented. (Case is dependent on the "case" directive option.) .. rst:directive:option:: case :type: "upper", "lower", or omitted Specifies whether to convert the full namespaced key to all uppercase, all lowercase, or leave it as is. .. rst:directive:: everett:component Defines an Everett component which is any Python class that has an inner class named ``Config`` which defines configuration options. The argument is the Python dotted path to the class. Add ``everett:option`` as part of the description. .. rst:directive:: everett:option Defines an Everett configuration option. The argument is the option key. .. rubric:: Options .. rst:directive:option:: parser :type: str The name of the parser for this option. .. rst:directive:option:: default :type: str If not set, the default is ``NO_VALUE`` which means that this option has no default value. If set, this is the default value. Enclose the value in double-quotes because all default values must be strings. .. rst:directive:option:: required :type: no value If set, this option is required. If not set and the option has a default, then this option is not required. If not set and the option has no default, then this option is required. This option is not required:: .. everett:option:: HOST :default: localhost These two options are required:: .. everett:option:: USERNAME .. everett:option:: PASSWORD :required: Examples ======== Documenting component configuration ----------------------------------- Here's an example Everett component: .. literalinclude:: ../examples/recipes_appconfig.py You can use the ``autocomponentconfig`` directive to extract the configuration information from the ``AppConfig`` class and document it:: .. autocomponentconfig:: recipes_appconfig.AppConfig :case: upper :show-table: That gives you something that looks like this: .. autocomponentconfig:: recipes_appconfig.AppConfig :case: upper :show-table: You can link to components with the ``:everett:component:`` role and options using the ``:everett:option:`` role. Example component link:: Component link: :everett:component:`recipes_appconfig.AppConfig` Component link: :everett:component:`recipes_appconfig.AppConfig` Example option link:: Option link: :everett:option:`recipes_appconfig.AppConfig.DEBUG` Option link: :everett:option:`recipes_appconfig.AppConfig.DEBUG` Documenting module configuration -------------------------------- You can use ``automoduleconfig`` to document configuration that's set at module import. This is helpful for Django settings modules. Example configuration code that sets up a :py:class:`everett.manager.ConfigManager` and calls it ``_config``: .. literalinclude:: ../examples/recipes_djangosettings.py :language: python Example documentation directive:: .. automoduleconfig:: recipes_djangosettings._config :hide-name: :case: upper :show-table: That gives you this: .. automoduleconfig:: recipes_djangosettings._config :hide-name: :case: upper :show-table: ================================================ FILE: docs/environments.rst ================================================ ========================== Configuration environments ========================== .. contents:: :local: Dict (ConfigDictEnv) ==================== .. autoclass:: everett.manager.ConfigDictEnv :noindex: Process environment (ConfigOSEnv) ================================= .. autoclass:: everett.manager.ConfigOSEnv :noindex: ENV files (ConfigEnvFileEnv) ============================ .. autoclass:: everett.manager.ConfigEnvFileEnv :noindex: Python objects (ConfigObjEnv) ============================= .. autoclass:: everett.manager.ConfigObjEnv :noindex: INI files (ConfigIniEnv) ======================== .. autoclass:: everett.ext.inifile.ConfigIniEnv :noindex: YAML files (ConfigYamlEnv) ========================== .. autoclass:: everett.ext.yamlfile.ConfigYamlEnv :noindex: Implementing your own configuration environments ================================================ You can implement your own configuration environments. For example, maybe you want to pull configuration from a database or Redis or a post-it note on the refrigerator. They just need to implement the ``.get()`` method. A no-op implementation is this: .. literalinclude:: ../examples/environments.py :language: python Generally, environments should return a value if the key exists in that environment and should return ``NO_VALUE`` if and only if the key does not exist in that environment. For exceptions, it depends on what you want to have happen. It's ok to let exceptions go unhandled--Everett will wrap them in a :py:class:`everett.ConfigurationError`. If your environment promises never to throw an exception, then you should handle them all and return ``NO_VALUE`` since with that promise all exceptions would indicate the key is not in the environment. ================================================ FILE: docs/history.rst ================================================ .. include:: ../HISTORY.rst ================================================ FILE: docs/index.rst ================================================ .. include:: ../README.rst Contents ======== .. toctree:: :maxdepth: 2 configmanager configuration parsers environments components documenting testing recipes api history dev Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: docs/parsers.rst ================================================ ======= Parsers ======= .. contents:: :local: What's a parser? ================ All parsers are functions that take a string value and return a parsed instance. For example: * ``int`` takes a string value and returns an int. * ``parse_class`` takes a string value that's a dotted Python path and returns the class object * ``ListOf(str)`` takes a string value that uses a comma delimiter and returns a list of strings .. Note:: When specifying configuration options, the default value must always be a string. When Everett can't find a value for a requested key, it will take the default value and pass it through the parser. Because parsers always take a string as input, the default value must always be a string. Good:: debug = config("debug", parser=bool, default="false") ^^^^^^^ Bad:: debug = config("debug", parser=bool, default=False) ^^^^^ Not a string Available parsers ================= Python types like str, int, float, pathlib.Path ----------------------------------------------- Python types can convert strings to Python values. You can use these as parsers: * ``str`` * ``int`` * ``float`` * ``decimal`` * ``pathlib.Path`` bools ----- Everett provides a special bool parser that handles more descriptive values for "true" and "false": * true: t, true, yes, y, on, 1 (and uppercase versions) * false: f, false, no, n, off, 0 (and uppercase versions) .. autofunction:: everett.manager.parse_bool :noindex: classes ------- Everett provides a ``everett.manager.parse_class`` that takes a string specifying a module and class and returns the class. .. autofunction:: everett.manager.parse_class :noindex: data size --------- Everett provides a ``everett.manager.parse_data_size`` that takes a string specifying an amount and a data size metric (e.g. kb, kib, tb, etc) and returns the amount of bytes that represents. .. autofunction:: everett.manager.parse_data_size :noindex: time period ----------- Everett provides a ``everett.manager.parse_time_period`` that takes a string specifying a period of time and returns the total number of seconds represented by that period. .. autofunction:: everett.manager.parse_data_size :noindex: ListOf(parser) -------------- Everett provides a special ``everett.manager.ListOf`` parser which parses a list of some other type. For example:: ListOf(str) # comma-delimited list of strings ListOf(int) # comma-delimited list of ints .. autofunction:: everett.manager.ListOf :noindex: ChoiceOf(parser, list-of-choices) --------------------------------- Everett provides a ``everett.manager.ChoiceOf`` parser which can enforce that configuration values belong to a specificed value domain. .. autofunction:: everett.manager.ChoiceOf :noindex: dj_database_url --------------- Everett works with `dj-database-url `_. The ``dj_database_url.parse`` function takes a string and returns a Django database connection value. For example:: import dj_database_url from everett.manager import ConfigManager config = ConfigManager.basic_config() DATABASES = { "default": config("DATABASE_URL", parser=dj_database_url.parse) } That'll pull the ``DATABASE_URL`` value from the environment (it throws an error if it's not there) and runs it through ``dj_database_url`` which parses it and returns what Django needs. With a default:: import dj_database_url from everett.manager import ConfigManager config = ConfigManager.basic_config() DATABASES = { "default": config( "DATABASE_URL", default="sqlite:///my.db", parser=dj_database_url.parse ) } .. Note:: To use dj-database-url, you'll need to install it separately. Everett doesn't depend on it or require it to be installed. django-cache-url ---------------- Everett works with `django-cache-url `_. For example:: import django_cache_url from everett.manager import ConfigManager config = ConfigManager.basic_config() CACHES = { "default": config("CACHE_URL", parser=django_cache_url.parse) } That'll pull the ``CACHE_URL`` value from the environment (it throws an error if it's not there) and runs it through ``django_cache_url`` which parses it and returns what Django needs. With a default:: import django_cache_url from everett.manager import ConfigManager config = ConfigManager.basic_config() CACHES = { "default": config( "CACHE_URL", default="locmem://myapp", parser=django_cache_url.parse ) } .. Note:: To use django-cache-url, you'll need to install it separately. Everett doesn't require it to be installed. Implementing your own parsers ============================= Implementing your own parser should be straight-forward. Parsing functions always take a string and return the Python value you need. If the value is not parseable, the parsing function should raise a ``ValueError``. For example, say we wanted to implement a parser that returned yes/no/no-answer or a parser class that's line delimited: .. literalinclude:: ../examples/parser_examples.py :language: python ================================================ FILE: docs/recipes.rst ================================================ ======= Recipes ======= This contains some ways of solving problems I've had with applications I use Everett in. These use cases help me to shape the Everett architecture such that it's convenient and flexible, but not big and overbearing. Hopefully they help you, too. If there are things you're trying to solve and you're using Everett that aren't covered here, add an item to the `issue tracker `_. .. contents:: :local: Centralizing configuration specification ======================================== It's easy to set up a :py:class:`everett.manager.ConfigManager` and then call it for configuration. However, with any non-trivial application, it's likely you're going to refer to configuration options multiple times in different parts of the code. One way to do this is to pull out the configuration value and store it in a global constant or an attribute somewhere and pass that around. Another way to do this is to create a configuration component, define all the configuration options there and then pass that component around. For example, this creates an ``AppConfig`` component which has configuration for the application: .. literalinclude:: ../examples/recipes_appconfig.py :language: python Couple of nice things here. First, is that if you do Sphinx documentation, you can use ``autocomponentconfig`` to automatically document your configuration based on the code. Second, you can use :py:func:`everett.manager.get_runtime_config` to print out the runtime configuration at startup. Using components that share configuration by passing arguments ============================================================== Say we have multiple components that share some configuration value that's probably managed by another component. For example, a "basedir" configuration value that defines the root directory for all the things this application does things with. Let's create an app component which creates two file system components passing them a basedir: .. literalinclude:: ../examples/recipes_shared.py :language: python Why do it this way? In this scenario, the ``basedir`` is defined at the app-scope and is passed to the reader and writer components when they're created. In this way, ``basedir`` is app configuration, but not reader/writer configuration. Using components that share configuration using alternate keys ============================================================== Say we have two components that share a set of credentials. We don't want to have to specify the same set of credentials twice, so instead, we use alternate keys which let you specify other keys to look at for a configuration value. This lets us have both components look at the same keys for their credentials and then we only have to define them once. Let's create a db reader and a db writer component: .. literalinclude:: ../examples/recipes_alternate_keys.py :language: python ================================================ FILE: docs/test_code.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Tests the code in the ../examples/ directory before including it in the docs. """ import os import subprocess import sys def main(): # FIXME(willkg): This is written to run on my machine. for fn in os.listdir("../examples/"): if not fn.endswith(".py"): continue print("Running %s..." % fn) subprocess.check_output(["python", "../examples/%s" % fn]) if __name__ == "__main__": sys.exit(main()) ================================================ FILE: docs/testing.rst ================================================ ======= Testing ======= You can test your code using ``config_override`` in your tests to test various configuration values. For example: .. literalinclude:: ../examples/testdebug.py :language: python .. autofunction:: everett.manager.config_override :noindex: ================================================ FILE: examples/component_appconfig.py ================================================ # component_appconfig.py from everett.manager import ConfigManager, Option # Central class holding configuration information class AppConfig: class Config: debug = Option( parser=bool, default="false", doc="Switch debug mode on and off.", ) # Build a ConfigManger to look at the process environment for # configuration and bound to the configuration options specified in # AppConfig def get_config(): manager = ConfigManager.basic_config( doc="Check https://example.com/configuration for docs." ) # Bind the configuration manager to the AppConfig component so that # it handles option properties like defaults, parsers, documentation, # and so on. return manager.with_options(AppConfig()) config = get_config() debug = config("debug") print(f"debug: {debug}") ================================================ FILE: examples/componentapp.py ================================================ # componentapp.py from everett.manager import ConfigManager, Option class S3Bucket: class Config: region = Option(doc="AWS S3 region") bucket_name = Option(doc="AWS S3 bucket name") def __init__(self, config): # Bind the configuration to just the configuration this component # requires such that this component is self-contained self.config = config.with_options(self) self.region = self.config("region") self.bucket_name = self.config("bucket_name") def repr(self): return f"" config = ConfigManager.from_dict( { "S3_SOURCE_REGION": "us-east-1", "S3_SOURCE_BUCKET_NAME": "mycompany_oldbucket", "S3_DEST_REGION": "us-east-1", "S3_DEST_BUCKET_NAME": "mycompany_newbucket", } ) s3_config = config.with_namespace("s3") source_bucket = S3Bucket(s3_config.with_namespace("source")) dest_bucket = S3Bucket(s3_config.with_namespace("dest")) print(repr(source_bucket)) print(repr(dest_bucket)) ================================================ FILE: examples/components_subclass.py ================================================ # components_subclass.py from everett.manager import ConfigManager, Option class ComponentA: class Config: foo = Option(default="foo_from_a") bar = Option(default="bar_from_a") class ComponentB(ComponentA): class Config: foo = Option(default="foo_from_b") def __init__(self, config): self.config = config.with_options(self) config = ConfigManager.basic_config() compb = ComponentB(config) print(compb.config("foo")) print(compb.config("bar")) ================================================ FILE: examples/environments.py ================================================ # environments.py from everett import NO_VALUE from everett.manager import listify class NoOpEnv(object): def get(self, key, namespace=None): # The namespace is either None, a string or a list of # strings. This converts it into a list. namespace = listify(namespace) # FIXME: Your code to extract the key in namespace here. # At this point, the key doesn't exist in the namespace # for this environment, so return a ``NO_VALUE``. return NO_VALUE ================================================ FILE: examples/handling_exceptions.py ================================================ # handling_exceptions.py import logging from everett import InvalidValueError from everett.manager import ConfigManager logging.basicConfig() config = ConfigManager.from_dict({"debug_mode": "monkey"}) try: some_val = config("debug_mode", parser=bool, doc="set debug mode") except InvalidValueError: print("I'm sorry dear user, but DEBUG_MODE must be 'true' or 'false'.") ================================================ FILE: examples/msg_builder.py ================================================ # msg_builder.py from everett.manager import ConfigManager, ConfigOSEnv def build_msg_for_ini(namespace, key, parser, msg="", option_doc="", config_doc=""): namespace = namespace or ["main"] namespace = "_".join(namespace) return f"Dear user. {key} in section [{namespace}] is not correct. Please fix it." config = ConfigManager( environments=[ConfigOSEnv()], msg_builder=build_msg_for_ini, ) config("debug", default="false", parser=bool) ================================================ FILE: examples/myserver.py ================================================ # myserver.py """ Minimal example showing how to use configuration for a web app. """ from everett.manager import ConfigManager config = ConfigManager.basic_config( doc="Check https://example.com/configuration for documentation." ) host = config("host", default="localhost") port = config("port", default="8000", parser=int) debug_mode = config( "debug", default="False", parser=bool, doc="Set to True for debugmode; False for regular mode", ) print(f"host: {host}") print(f"port: {port}") print(f"debug_mode: {debug_mode}") ================================================ FILE: examples/myserver_with_environments.py ================================================ # myserver_with_environments.py """ Minimal example showing how to use configuration for a web app that pulls configuration from specified environments. """ import os from everett.ext.inifile import ConfigIniEnv from everett.manager import ConfigManager, ConfigOSEnv, ConfigDictEnv config = ConfigManager( [ # Pull from the OS environment first ConfigOSEnv(), # Fall back to the file specified by the FOO_INI OS environment # variable if such file exists ConfigIniEnv(os.environ.get("FOO_INI")), # Fall back to this dict of defaults ConfigDictEnv({"FOO_VAR": "bar"}), ], doc="Check https://example.com/configuration for documentation.", ) host = config("host", default="localhost") port = config("port", default="8000", parser=int) debug_mode = config( "debug", default="False", parser=bool, doc="Set to True for debugmode; False for regular mode", ) print(f"host: {host}") print(f"port: {port}") print(f"debug_mode: {debug_mode}") ================================================ FILE: examples/namespaces.py ================================================ # namespaces.py from everett.manager import ConfigManager def open_connection(config): username = config("username") password = config("password") port = config("port", default="5432", parser=int) print(f"Opened database with {username}/{password} on port {port}") config = ConfigManager.from_dict( { "DB_USERNAME": "admin", "DB_PASSWORD": "ou812", } ) # Database configuration keys are all prefixed with "db", so we want to # retrieve database configuration keys with the "db" namespace db_config = config.with_namespace("db") open_connection(db_config) ================================================ FILE: examples/namespaces2.py ================================================ # namespaces2.py from everett.manager import ConfigManager def open_connection(config): username = config("username") password = config("password") port = config("port", default="5432", parser=int) print(f"Opened database with {username}/{password} on port {port}") config = ConfigManager.from_dict( { "SOURCE_DB_USERNAME": "admin", "SOURCE_DB_PASSWORD": "ou812", "DEST_DB_USERNAME": "admin", "DEST_DB_PASSWORD": "P9rwvnnj8CidECMb", } ) # Database configuration keys are all prefixed with "db", so we want to # retrieve database configuration keys with the "db" namespace source_db_config = config.with_namespace("source_db") dest_db_config = config.with_namespace("dest_db") source_conn = open_connection(source_db_config) dest_conn = open_connection(dest_db_config) ================================================ FILE: examples/parser_examples.py ================================================ # parser_examples.py from everett.manager import ConfigManager, get_parser def parse_ynm(val): """Returns True, False or None (empty string)""" val = val.strip().lower() if not val: return None return val[0] == "y" config = ConfigManager.from_dict( {"NO_ANSWER": "", "YES": "yes", "ALSO_YES": "y", "NO": "no"} ) assert config("no_answer", parser=parse_ynm) is None assert config("yes", parser=parse_ynm) is True assert config("also_yes", parser=parse_ynm) is True assert config("no", parser=parse_ynm) is False class Pairs(object): def __init__(self, val_parser): self.val_parser = val_parser def __call__(self, val): val_parser = get_parser(self.val_parser) out = [] for part in val.split(","): k, v = part.split(":") out.append((k, val_parser(v))) return out config = ConfigManager.from_dict({"FOO": "a:1,b:2,c:3"}) assert config("FOO", parser=Pairs(int)) == [("a", 1), ("b", 2), ("c", 3)] ================================================ FILE: examples/recipes_alternate_keys.py ================================================ # recipes_alternate_keys.py from everett.manager import ConfigManager, Option class DatabaseReader: class Config: username = Option(alternate_keys=["root:db_username"]) password = Option(alternate_keys=["root:db_password"]) def __init__(self, config): self.config = config.with_options(self) class DatabaseWriter: class Config: username = Option(alternate_keys=["root:db_username"]) password = Option(alternate_keys=["root:db_password"]) def __init__(self, config): self.config = config.with_options(self) # Define a shared configuration config = ConfigManager.from_dict({"DB_USERNAME": "foo", "DB_PASSWORD": "bar"}) reader = DatabaseReader(config.with_namespace("reader")) assert reader.config("username") == "foo" assert reader.config("password") == "bar" writer = DatabaseWriter(config.with_namespace("writer")) assert writer.config("username") == "foo" assert writer.config("password") == "bar" # Or define different credentials config = ConfigManager.from_dict( { "READER_USERNAME": "joe", "READER_PASSWORD": "foo", "WRITER_USERNAME": "pete", "WRITER_PASSWORD": "bar", } ) reader = DatabaseReader(config.with_namespace("reader")) assert reader.config("username") == "joe" assert reader.config("password") == "foo" writer = DatabaseWriter(config.with_namespace("writer")) assert writer.config("username") == "pete" assert writer.config("password") == "bar" ================================================ FILE: examples/recipes_appconfig.py ================================================ # recipes_appconfig.py import logging from everett.manager import ConfigManager, Option TEXT_TO_LOGGING_LEVEL = { "CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10, } def parse_loglevel(value): try: return TEXT_TO_LOGGING_LEVEL[value.upper()] except KeyError as exc: raise ValueError( f'"{value}" is not a valid logging level. Try CRITICAL, ERROR, ' "WARNING, INFO, DEBUG" ) from exc class AppConfig: class Config: debug = Option( parser=bool, default="false", doc="Turns on debug mode for the application", ) loglevel = Option( parser=parse_loglevel, default="INFO", doc=( "Log level for the application; CRITICAL, ERROR, WARNING, INFO, DEBUG" ), ) def init_app(): manager = ConfigManager.from_dict({}) config = manager.with_options(AppConfig()) logging.basicConfig(level=config("loglevel")) if config("debug"): logging.info("debug mode!") if __name__ == "__main__": init_app() ================================================ FILE: examples/recipes_djangosettings.py ================================================ # recipes_djangosettings.py from everett.manager import ConfigManager _config = ConfigManager.basic_config() DEBUG = _config( "debug", parser=bool, default="False", doc="Whether or not DEBUG mode is enabled." ) CACHES = { "default": { "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", "LOCATION": _config( "cache_location", default="127.0.0.1:11211", doc="Memcache cache location." ), "TIMEOUT": _config( "cache_timeout", default="500", parser=int, doc="Timeout to use when accessing cache.", ), "KEY_PREFIX": _config( "cache_key_prefix", default="socorro", doc="Key prefix to use for all cache keys.", ), } } ================================================ FILE: examples/recipes_shared.py ================================================ # recipes_shared.py import os from everett.manager import ConfigManager, Option, parse_class class App: class Config: basedir = Option() reader = Option(parser=parse_class) writer = Option(parser=parse_class) def __init__(self, config): self.config = config.with_options(self) self.basedir = self.config("basedir") self.reader = self.config("reader")(config, self.basedir) self.writer = self.config("writer")(config, self.basedir) class FilesystemReader: class Config: file_type = Option(default="json") def __init__(self, config, basedir): self.config = config.with_options(self) self.read_dir = os.path.join(basedir, "read") class FilesystemWriter: class Config: file_type = Option(default="json") def __init__(self, config, basedir): self.config = config.with_options(self) self.write_dir = os.path.join(basedir, "write") config = ConfigManager.from_dict( { "BASEDIR": "/tmp", "READER": "__main__.FilesystemReader", "WRITER": "__main__.FilesystemWriter", "READER_FILE_TYPE": "json", "WRITER_FILE_TYPE": "yaml", } ) app = App(config) assert app.reader.read_dir == "/tmp/read" assert app.writer.write_dir == "/tmp/write" ================================================ FILE: examples/testdebug.py ================================================ # testdebug.py """ Minimal example showing how to override configuration values when testing. """ import unittest from everett.manager import ConfigManager, config_override class App: def __init__(self): config = ConfigManager.basic_config() self.debug = config("debug", default="False", parser=bool) class TestDebug(unittest.TestCase): def test_debug_on(self): with config_override(DEBUG="on"): app = App() self.assertTrue(app.debug) def test_debug_off(self): with config_override(DEBUG="off"): app = App() self.assertFalse(app.debug) if __name__ == "__main__": unittest.main() ================================================ FILE: justfile ================================================ sphinxbuild := "../.venv/bin/sphinx-build" @_default: just --list # Build a development environment devenv: uv sync --extra sphinx --extra ini --extra yaml --extra dev --refresh --upgrade # Run tests, linting, and static typechecking test: devenv uv run tox # Format files format: devenv uv run tox exec -e py310-lint -- ruff format # Lint files lint: devenv uv run tox -e py310-lint # Wipe devenv and build artifacts clean: rm -rf .venv uv.lock rm -rf build dist src/everett.egg-info .tox .pytest_cache .mypy_cache rm -rf docs/_build/* find src/ tests/ -name __pycache__ | xargs rm -rf find src/ tests/ -name '*.pyc' | xargs rm -rf # Runs cog and builds Sphinx docs docs: devenv uv run python -m cogapp -r README.rst uv run python -m cogapp -r docs/components.rst uv run python -m cogapp -r docs/configmanager.rst uv run python -m cogapp -r docs/configuration.rst SPHINXBUILD={{sphinxbuild}} make -e -C docs/ clean SPHINXBUILD={{sphinxbuild}} make -e -C docs/ doctest SPHINXBUILD={{sphinxbuild}} make -e -C docs/ html # Build files for relase build: devenv rm -rf build/ dist/ uv run python -m build uv run twine check dist/* ================================================ FILE: pyproject.toml ================================================ [project] name = "everett" description = "Configuration library for Python applications" version = "3.5.0" readme = "README.rst" keywords = ["conf", "config", "configuration", "ini", "env", "yaml"] authors = [{name = "Will Kahn-Greene"}] license = {text = "MPLv2"} requires-python = ">=3.10" dependencies = [] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Natural Language :: English", "Programming Language :: Python :: 3 :: Only", "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", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Python Modules", ] urls.Homepage = "https://everett.readthedocs.io/" urls.Source = "https://github.com/willkg/everett/" urls.Issues = "https://github.com/willkg/everett/issues" [project.optional-dependencies] sphinx = [ "sphinx", ] ini = [ "configobj", ] yaml = [ "PyYAML", ] dev = [ "build", "cogapp", "mypy", "pytest", "ruff", "tox", "tox-gh-actions", "tox-uv", "twine", "types-PyYAML", "Sphinx==7.2.6", "sphinx_rtd_theme", ] [build-system] requires = ["setuptools", "setuptools-scm"] build-backend = "setuptools.build_meta" [tool.ruff] target-version = "py310" src = ["src"] line-length = 88 [tool.ruff.lint] # Enable pycodestyle (E), pyflakes (F), and bugbear (B) rules select = ["E", "F", "B"] # Ignore line length violations; ruff format does its best and we can rely on # that ignore = ["E501"] [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" [tool.mypy] python_version = "3.10" disallow_untyped_defs = true [[tool.mypy.overrides]] module = "configobj.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "docutils.*" ignore_missing_imports = true [tool.pytest.ini_options] filterwarnings = [ "error", "ignore:::babel[.*]", "ignore:::jinja2[.*]", "ignore:::yaml[.*]", # Sphinx 4.2.0 uses distutils and it's deprecated in 3.10 "ignore::DeprecationWarning:sphinx", ] [tool.tox] legacy_tox_ini = """ [tox] envlist = py310 py311 py312 py313 py314 py310-doctest py310-lint py310-typecheck uv_python_preference = only-managed [gh-actions] python = 3.10: py310 3.11: py311 3.12: py312 3.13: py313 3.14: py314 [testenv] extras = dev,ini,sphinx,yaml commands = pytest {posargs} tests/ [testenv:py310-doctest] basepython = python3.10 commands = pytest --doctest-modules src/ [testenv:py310-lint] allowlist_externals = ruff basepython = python3.10 changedir = {toxinidir} commands = ruff format --check tests docs examples ruff check src tests docs examples [testenv:py310-typecheck] basepython = python3.10 changedir = {toxinidir} commands = mypy src/everett/ """ ================================================ FILE: src/everett/__init__.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Everett is a Python library for configuration.""" from importlib.metadata import ( version as importlib_version, PackageNotFoundError, ) from typing import Callable, Union try: __version__ = importlib_version("everett") except PackageNotFoundError: __version__ = "unknown" __all__ = [ "NO_VALUE", "ConfigurationError", "DetailedConfigurationError", "InvalidKeyError", "InvalidValueError", "ConfigurationMissingError", ] # NoValue instances are always false class NoValue: def __nonzero__(self) -> bool: return False def __bool__(self) -> bool: return False def __repr__(self) -> str: return "NO_VALUE" #: Singleton indicating a non-value. NO_VALUE = NoValue() class ConfigurationError(Exception): """Configuration error base class.""" pass class InvalidKeyError(ConfigurationError): """Error that indicates the key is not valid for this component.""" pass class DetailedConfigurationError(ConfigurationError): """Base class for configuration errors that have a msg, namespace, key, and parser.""" def __init__( self, msg: str, namespace: Union[list[str], None], key: str, parser: Callable ): self.msg = msg self.namespace = namespace self.key = key self.parser = parser super().__init__(msg, namespace, key, parser) def __str__(self) -> str: return self.msg class ConfigurationMissingError(DetailedConfigurationError): """Error that indicates that required configuration is missing.""" pass class InvalidValueError(DetailedConfigurationError): """Error that indicates that the value is not valid. .. Note:: Parsers should not raise this exception. Parsers should raise ``ValueError`` when the value is not a valid value. """ pass ================================================ FILE: src/everett/ext/__init__.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Holds env files that have other requirements.""" ================================================ FILE: src/everett/ext/inifile.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Holds the ConfigIniEnv environment. To use this, you must install the optional requirements:: $ pip install 'everett[ini]' """ import logging import os from typing import Optional, Union from configobj import ConfigObj from everett import NO_VALUE, NoValue from everett.manager import generate_uppercase_key, get_key_from_envs, listify logger = logging.getLogger("everett") class ConfigIniEnv: """Source for pulling configuration from INI files. This requires optional dependencies. You can install them with:: $ pip install 'everett[ini]' Takes a path or list of possible paths to look for a INI file. It uses the first INI file it can find. If it finds no INI files in the possible paths, then this configuration source will be a no-op. This will expand ``~`` as well as work relative to the current working directory. This example looks just for the INI file specified in the environment:: from everett.manager import ConfigManager from everett.ext.inifile import ConfigIniEnv config = ConfigManager([ ConfigIniEnv(possible_paths=os.environ.get("FOO_INI")) ]) If there's no ``FOO_INI`` in the environment, then the path will be ignored. Here's an example that looks for the INI file specified in the environment variable ``FOO_INI`` and failing that will look for ``.antenna.ini`` in the user's home directory:: from everett.manager import ConfigManager from everett.ext.inifile import ConfigIniEnv config = ConfigManager([ ConfigIniEnv( possible_paths=[ os.environ.get("FOO_INI"), "~/.antenna.ini" ] ) ]) This example looks for a ``config/local.ini`` file which overrides values in a ``config/base.ini`` file both are relative to the current working directory:: from everett.manager import ConfigManager from everett.ext.inifile import ConfigIniEnv config = ConfigManager([ ConfigIniEnv(possible_paths="config/local.ini"), ConfigIniEnv(possible_paths="config/base.ini") ]) Note how you can have multiple ``ConfigIniEnv`` files and this is how you can set Everett up to have values in one INI file override values in another INI file. INI files must have a "main" section. This is where keys that aren't in a namespace are placed. Minimal INI file:: [main] In the INI file, namespace is a section. So key "user" in namespace "foo" is:: [foo] user=someval Everett uses configobj, so it supports nested sections like this:: [main] foo=bar [namespace] foo2=bar2 [[namespace2]] foo3=bar3 Which gives you these: * ``FOO`` * ``NAMESPACE_FOO2`` * ``NAMESPACE_NAMESPACE2_FOO3`` See more details here: http://configobj.readthedocs.io/en/latest/configobj.html#the-config-file-format """ def __init__(self, possible_paths: Union[str, list[str]]) -> None: """ :param possible_paths: either a single string with a file path (e.g. ``"/etc/project.ini"`` or a list of strings with file paths """ self.cfg = {} self.path = None possible_paths = listify(possible_paths) for path in possible_paths: if not path: continue path = os.path.abspath(os.path.expanduser(path.strip())) if path and os.path.isfile(path): self.path = path self.cfg.update(self.parse_ini_file(path)) break if not self.path: logger.debug("No INI file found: %s", possible_paths) def parse_ini_file(self, path: str) -> dict: """Parse ini file at ``path`` and return dict.""" cfgobj = ConfigObj(path, list_values=False) def extract_section(namespace: list[str], d: dict) -> dict: cfg = {} for key, val in d.items(): if isinstance(d[key], dict): cfg.update(extract_section(namespace + [key], d[key])) else: cfg["_".join(namespace + [key]).upper()] = val return cfg return extract_section([], cfgobj.dict()) def get( self, key: str, namespace: Optional[list[str]] = None ) -> Union[str, NoValue]: """Retrieve value for key.""" if not self.path: return NO_VALUE # NOTE(willkg): The "main" section is considered the root mainspace. namespace = namespace or ["main"] logger.debug("Searching %r for key: %s, namespace: %s", self, key, namespace) full_key = generate_uppercase_key(key, namespace) return get_key_from_envs(self.cfg, full_key) def __repr__(self) -> str: return "" % self.path ================================================ FILE: src/everett/ext/yamlfile.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Holds the ConfigYamlEnv environment. To use this, you must install the optional requirements:: $ pip install 'everett[yaml]' """ import logging import os from typing import Optional, Union import yaml from everett import ConfigurationError, NO_VALUE, NoValue from everett.manager import generate_uppercase_key, get_key_from_envs, listify logger = logging.getLogger("everett") class ConfigYamlEnv: """Source for pulling configuration from YAML files. This requires optional dependencies. You can install them with:: $ pip install 'everett[yaml]' Takes a path or list of possible paths to look for a YAML file. It uses the first YAML file it can find. If it finds no YAML files in the possible paths, then this configuration source will be a no-op. This will expand ``~`` as well as work relative to the current working directory. This example looks just for the YAML file specified in the environment:: from everett.manager import ConfigManager from everett.ext.yamlfile import ConfigYamlEnv config = ConfigManager([ ConfigYamlEnv(os.environ.get('FOO_YAML')) ]) If there's no ``FOO_YAML`` in the environment, then the path will be ignored. Here's an example that looks for the YAML file specified in the environment variable ``FOO_YAML`` and failing that will look for ``.antenna.yaml`` in the user's home directory:: from everett.manager import ConfigManager from everett.ext.yamlfile import ConfigYamlEnv config = ConfigManager([ ConfigYamlEnv([ os.environ.get('FOO_YAML'), '~/.antenna.yaml' ]) ]) This example looks for a ``config/local.yaml`` file which overrides values in a ``config/base.yaml`` file both are relative to the current working directory:: from everett.manager import ConfigManager from everett.ext.yamlfile import ConfigYamlEnv config = ConfigManager([ ConfigYamlEnv('config/local.yaml'), ConfigYamlEnv('config/base.yaml') ]) Note how you can have multiple ``ConfigYamlEnv`` files. This is how you can set Everett up to have values in one YAML file override values in another YAML file. Everett looks for keys and values in YAML files. YAML files can be split into multiple documents, but Everett only looks at the first one. Keys are case-insensitive. You can do namespaces either in the key itself using ``_`` as a separator or as nested mappings. All values should be double-quoted. Here's an example:: foo: "bar" FOO2: "bar" namespace_foo: "bar" namespace: namespace2: foo: "bar" Giving you these namespaced keys: * ``FOO`` * ``FOO2`` * ``NAMESPACE_FOO`` * ``NAMESPACE_NAMEPSACE2_FOO`` """ def __init__(self, possible_paths: Union[str, list[str]]) -> None: """ :param possible_paths: either a single string with a file path (e.g. ``"/etc/project.yaml"`` or a list of strings with file paths """ self.cfg = {} self.path = None possible_paths = listify(possible_paths) for path in possible_paths: if not path: continue path = os.path.abspath(os.path.expanduser(path.strip())) if path and os.path.isfile(path): self.path = path self.cfg = self.parse_yaml_file(path) break if not self.path: logger.debug("No YAML file found: %s", possible_paths) def parse_yaml_file(self, path: str) -> dict: """Parse yaml file at ``path`` and return a dict.""" with open(path) as fp: data = yaml.safe_load(fp) if not data: return {} def traverse(namespace: list[str], d: dict) -> dict: cfg = {} for key, val in d.items(): if isinstance(val, dict): cfg.update(traverse(namespace + [key], val)) elif isinstance(val, str): cfg["_".join(namespace + [key]).upper()] = val else: # All values should be double-quoted strings so they # parse as strings; anything else is a configuration # error at parse-time raise ConfigurationError( "Invalid value %r in file %s: values must be double-quoted strings" % (val, path) ) return cfg return traverse([], data) def get( self, key: str, namespace: Optional[list[str]] = None ) -> Union[str, NoValue]: """Retrieve value for key.""" if not self.path: return NO_VALUE logger.debug("Searching %r for key: %s, namepsace: %s", self, key, namespace) full_key = generate_uppercase_key(key, namespace) return get_key_from_envs(self.cfg, full_key) def __repr__(self) -> str: return "" % self.path ================================================ FILE: src/everett/manager.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Contains configuration infrastructure. This module contains the configuration classes and functions for deriving configuration values from specified sources in the order specified. """ from functools import wraps import importlib import inspect import logging import os import re import sys from types import TracebackType from typing import ( Any, Callable, Optional, Union, ) from collections.abc import Iterable, Mapping from everett import ( ConfigurationError, ConfigurationMissingError, InvalidValueError, InvalidKeyError, NO_VALUE, NoValue, ) __all__ = [ "ChoiceOf", "ConfigDictEnv", "ConfigEnvFileEnv", "ConfigManager", "ConfigObjEnv", "ConfigOSEnv", "config_override", "get_config_for_class", "get_runtime_config", "ListOf", "Option", "parse_bool", "parse_class", "parse_data_size", "parse_env_file", "parse_time_period", ] # Regex for valid keys in an env file ENV_KEY_RE = re.compile(r"^[a-z_][a-z0-9_]*$", flags=re.IGNORECASE) logger = logging.getLogger("everett") def qualname(thing: Any) -> str: """Return the Python dotted name for a given thing. >>> import everett.manager >>> qualname(str) 'str' >>> qualname(everett.manager.parse_class) 'everett.manager.parse_class' >>> qualname(everett.manager) 'everett.manager' :param thing: the thing to get the qualname from :returns: the Python dotted name """ parts = [] # Add the module, unless it's a builtin mod = inspect.getmodule(thing) if mod and mod.__name__ not in ("__main__", "__builtin__", "builtins"): parts.append(mod.__name__) if hasattr(thing, "__qualname__"): parts.append(thing.__qualname__) return ".".join(parts) # If it's a module if inspect.ismodule(thing): return ".".join(parts) # It's an instance, so ... let's call repr on it return repr(thing) def build_msg( namespace: Optional[list[str]], key: Optional[str], parser: Optional[Callable], msg: str = "", option_doc: str = "", config_doc: str = "", ) -> str: """Builds a message for a configuration error exception :param namespace: list of strings that represent the configuration variable namespace or ``None`` :param key: the configuration variable key or ``None`` :param parser: the parser that will be used to parse the value for this configuration variable or ``None`` :param msg: the error message :param option_doc: the configuration option documentation :param config_doc: the ConfigManager documentation :returns: the error message string """ text = [msg] if key and parser: full_key = generate_uppercase_key(key, namespace) text.append(f"{full_key} requires a value parseable by {qualname(parser)}") else: full_key = None if option_doc and full_key: text.append(f"{full_key} docs: {option_doc}") if config_doc: text.append(f"Project docs: {config_doc}") return "\n".join([line for line in text if line]) # FIXME(willkg): we can rewrite this as a dataclass as soon as we can drop # Python 3.6 support class Option: """Settings for a single configuration option. Use this when creating Everett configuration components. Example:: from everett.manager import Option class MyService: # Note: The Config class has to be called "Config". class Config: host = Option(default="localhost") port = Option(default="8000", parser=int) """ def __init__( self, default: Union[str, NoValue] = NO_VALUE, alternate_keys: Optional[list[str]] = None, doc: str = "", parser: Callable = str, meta: Any = None, ): """ :param default: the default value (if any); this must be a string that is parseable by the specified parser; if no default is provided, this will raise an error or return ``everett.NO_VALUE`` depending on the value of ``raise_error`` :param alternate_keys: the list of alternate keys to look up; supports a ``root:`` key prefix which will cause this to look at the configuration root rather than the current namespace .. versionadded:: 0.3 :param doc: documentation for this config option .. versionadded:: 0.6 :param parser: the parser for converting this value to a Python object :param meta: any meta information that's tied to this option; useful for noting which options are related in some way or which are secrets that shouldn't be logged """ self.default = default self.alternate_keys = alternate_keys self.doc = doc self.parser = parser self.meta = meta or {} def __eq__(self, obj: Any) -> bool: return ( isinstance(obj, Option) and obj.default == self.default and obj.alternate_keys == self.alternate_keys and obj.doc == self.doc and obj.parser == self.parser and obj.meta == self.meta ) def get_config_for_class(cls: type) -> dict[str, tuple[Option, type]]: """Roll up configuration options for this class and parent classes. This handles subclasses overriding configuration options in parent classes. :param cls: the component class to return configuration options for :returns: final dict of configuration options for this class in ``key -> (option, cls)`` form """ options = {} for subcls in reversed(cls.__mro__): if not hasattr(subcls, "Config"): continue subcls_config = subcls.Config for attr in subcls_config.__dict__.keys(): if attr.startswith("__"): continue val = getattr(subcls_config, attr) if isinstance(val, Option): options[attr] = (val, subcls) return options def traverse_tree( instance: Any, namespace: Optional[list[str]] = None ) -> Iterable[tuple[list[str], str, Option, Any]]: """Traverses a tree of objects and computes the configuration for it Note: This expects the tree not to have any loops or repeated nodes. :param instance: the component to traverse :param namespace: the list of strings forming the namespace or None :returns: list of ``(namespace, key, value, option, component)`` """ namespace = namespace or [] # Check to see if this class has options; if it does, capture those and # traverse the tree this_options = get_config_for_class(instance.__class__) if not this_options: return [] options = [ (namespace, key, option, instance) for key, (option, cls) in this_options.items() ] # Now go through attributes for other options classes for attr in dir(instance): if attr.startswith("__"): continue # NOTE(willkg): we skip slots; maybe they could be component classes, # but that seems bizarre and I'd like to see a reasonable example # before supporting it val = getattr(instance, attr, None) if not val or isinstance(val, Option): continue options.extend(traverse_tree(val, namespace + [attr])) return options def parse_env_file(envfile: Iterable[str]) -> dict: """Parse the content of an iterable of lines as ``.env``. Return a dict of config variables. >>> from everett.manager import parse_env_file >>> parse_env_file(["DUDE=Abides"]) {'DUDE': 'Abides'} """ data = {} for line_no, line in enumerate(envfile): line = line.strip() if not line or line.startswith("#"): continue if "=" not in line: raise ConfigurationError( f"Env file line missing = operator (line {line_no + 1})" ) k, v = line.split("=", 1) k = k.strip() if not ENV_KEY_RE.match(k): raise ConfigurationError( f"Invalid variable name {k!r} in env file (line {line_no + 1})" ) v = v.strip() # Need to strip matching ' and " from beginning and end--but only one # round for quote in "'\"": if v.startswith(quote) and v.endswith(quote): v = v[1:-1] break data[k] = v return data def parse_bool(val: str) -> bool: """Parse a bool value. Handles a series of values, but you should probably standardize on "true" and "false". >>> from everett.manager import parse_bool >>> parse_bool("y") True >>> parse_bool("FALSE") False """ true_vals = ("t", "true", "yes", "y", "1", "on") false_vals = ("f", "false", "no", "n", "0", "off") val = val.lower() if val in true_vals: return True if val in false_vals: return False raise ValueError(f"{val!r} is not a valid bool value") def parse_class(val: str) -> Any: """Parse a string, imports the module and returns the class. >>> from everett.manager import parse_class >>> parse_class("everett.manager.Option") """ if "." not in val: raise ValueError(f"{val!r} is not a valid Python dotted-path") module_name, class_name = val.rsplit(".", 1) module = importlib.import_module(module_name) try: return getattr(module, class_name) except AttributeError as exc: raise ValueError( f"{class_name!r} is not a valid member of {qualname(module)}" ) from exc _DATA_SIZE_METRIC_TO_MULTIPLIER = { "": 1, "b": 1, "kb": 1_000, "mb": pow(1_000, 2), "gb": pow(1_000, 3), "tb": pow(1_000, 4), "kib": 1_024, "mib": pow(1_024, 2), "gib": pow(1_024, 3), "tib": pow(1_024, 4), } _DATA_SIZE_RE = re.compile( r"^([0-9_]+)(" + "|".join(_DATA_SIZE_METRIC_TO_MULTIPLIER.keys()) + ")?$" ) def parse_data_size(val: str) -> Any: """Parse a string denoting a data size into an int of bytes. This allows you to parse data sizes with a number and then the metric. Examples: * 10b - 10 bytes * 100kb - 100 kilobytes = 100 * 1000 * 40gb - 40 gigabytes = 40 * 1000^3 * 23gib - 40 gibibytes = 23 * 1024^3 Supported metrics: * b - bytes * decimal: * kb - kilobytes * mb - megabytes * gb - gigabytes * tb - terabytes * pb - petabytes * binary: * kib - kibibytes * mib - mebibytes * gib - gibibytes * tib - tebibytes * pib - pebibytes The metrics are not case sensitive--it supports upper, lower, and mixed case. >>> from everett.manager import parse_data_size >>> parse_data_size("40_000_000") 40000000 >>> parse_data_size("40gb") 40000000000 >>> parse_data_size("20KiB") 20480 """ fixed_val = val.lower().strip() try: return int(fixed_val) except ValueError: pass match = _DATA_SIZE_RE.match(fixed_val) if not match: raise ValueError(f"{val!r} is not a valid data size") amount, metric = match.groups() return int(amount) * _DATA_SIZE_METRIC_TO_MULTIPLIER[metric] _TIME_UNIT_TO_MULTIPLIER = { "w": 7 * 24 * 60 * 60, "d": 24 * 60 * 60, "h": 60 * 60, "m": 60, "s": 1, } _TIME_RE = re.compile(r"([0-9_]+)([" + "".join(_TIME_UNIT_TO_MULTIPLIER.keys()) + r"])") def parse_time_period(val: str) -> Any: """Parse a string denoting a time period into a number of seconds. Units: * w - week * d - day * h - hour * m - minute * s - second >>> from everett.manager import parse_time_period >>> parse_time_period("103") 103 >>> parse_time_period("1_000m") 60000 >>> parse_time_period("15m4s") 904 """ fixed_val = val.lower().strip() try: return int(fixed_val) except ValueError: pass parts = _TIME_RE.findall(fixed_val) if not parts: raise ValueError(f"{val!r} is not a valid time period") total = 0 for part in parts: amount, unit = part total = total + (int(amount) * _TIME_UNIT_TO_MULTIPLIER[unit]) return total def get_parser(parser: Callable) -> Callable: """Return a parsing function for a given parser.""" # Special case bool so that we can explicitly give bool values otherwise # all values would be True since they're non-empty strings. if parser is bool: return parse_bool return parser def listify(thing: Any) -> list[Any]: """Convert thing to a list. If thing is a string, then returns a list of thing. Otherwise returns thing. :param thing: string or list of things :returns: list """ if thing is None: return [] if isinstance(thing, str): return [thing] return thing def generate_uppercase_key(key: str, namespace: Optional[list[str]] = None) -> str: """Given a key and a namespace, generates a final uppercase key. >>> generate_uppercase_key("foo") 'FOO' >>> generate_uppercase_key("foo", ["namespace"]) 'NAMESPACE_FOO' >>> generate_uppercase_key("foo", ["namespace", "subnamespace"]) 'NAMESPACE_SUBNAMESPACE_FOO' """ if namespace: namespace = [part for part in listify(namespace) if part] key = "_".join(namespace + [key]) key = key.upper() return key def get_key_from_envs(envs: Iterable[Any], key: str) -> Union[str, NoValue]: """Return the value of a key from the given dict respecting namespaces. Data can also be a list of data dicts. """ # if it barks like a dict, make it a list have to use `get` since dicts and # lists both have __getitem__ if hasattr(envs, "get"): envs = [envs] for env in envs: if key in env: return env[key] return NO_VALUE class ListOf: """Parse a delimiter-separated list of things. After delimiting items, this strips the whitespace at the beginning and end of each string. Then it passes each string into the parser to get the final value. >>> from everett.manager import ListOf >>> ListOf(str)('') [] >>> ListOf(str)('a,b,c,d') ['a', 'b', 'c', 'd'] >>> ListOf(int)('1,2,3,4') [1, 2, 3, 4] >>> ListOf(str)('1, 2 ,3,4') ['1', '2', '3', '4'] ``ListOf`` defaults to using a comma as a delimiter, but supports other delimiters: >>> ListOf(str, delimiter=":")("/path/a/:/path/b/") ['/path/a/', '/path/b/'] ``ListOf`` supports raising a configuration error when one of the values is an empty string: >>> ListOf(str, allow_empty=False)("a,,b") Traceback (most recent call last): ... ValueError: 'a,,b' can not have empty values The user will get a configuration error like this:: ValueError: 'a,,b' can not have empty values NAMES requires a value parseable by Note: This doesn't handle quotes or backslashes or any complicated string parsing. For example: >>> ListOf(str)('"a,b",c,d') ['"a', 'b"', 'c', 'd'] """ def __init__( self, parser: Callable, delimiter: str = ",", allow_empty: bool = True ): self.sub_parser = parser self.delimiter = delimiter self.allow_empty = allow_empty def __call__(self, value: str) -> list[Any]: parser = get_parser(self.sub_parser) if value: parsed_values = [] for token in value.split(self.delimiter): token = token.strip() if not token and not self.allow_empty: raise ValueError(f"{value!r} can not have empty values") parsed_values.append(parser(token)) return parsed_values else: return [] def __repr__(self) -> str: return ( f"" ) class ChoiceOf: """Parser that enforces values are in a specified value domain. Choices can be a list of string values that are parseable by the sub parser. For example, say you only supported two cloud providers and need the configuration value to be one of "aws" or "gcp": >>> from everett.manager import ChoiceOf >>> ChoiceOf(str, choices=["aws", "gcp"])("aws") 'aws' Choices works with the int sub-parser: >>> from everett.manager import ChoiceOf >>> ChoiceOf(int, choices=["1", "2", "3"])("1") 1 Choices works with any sub-parser: >>> from everett.manager import ChoiceOf, parse_data_size >>> ChoiceOf(parse_data_size, choices=["1kb", "1mb", "1gb"])("1mb") 1000000 Note: The choices list is a list of strings--these are values before being parsed. This makes it easier for people who are doing configuration to know what the values they put in their configuration files need to look like. """ def __init__(self, parser: Callable, choices: list[str]): self.sub_parser = parser if not choices or not all(isinstance(choice, str) for choice in choices): raise ValueError(f"choices {choices!r} must be a non-empty list of strings") self.choices = choices def __call__(self, value: str) -> Any: parser = get_parser(self.sub_parser) if value and value in self.choices: return parser(value) raise ValueError(f"{value!r} is not a valid choice") def __repr__(self) -> str: return f"" class ConfigOverrideEnv: """Override configuration layer for testing.""" def get( self, key: str, namespace: Optional[list[str]] = None ) -> Union[str, NoValue]: """Retrieve value for key.""" global _CONFIG_OVERRIDE # Short-circuit to reduce overhead. if not _CONFIG_OVERRIDE: return NO_VALUE full_key = generate_uppercase_key(key, namespace) logger.debug(f"Searching {self!r} for {full_key}") return get_key_from_envs(reversed(_CONFIG_OVERRIDE), full_key) def __repr__(self) -> str: return "" class ConfigObjEnv: """Source for pulling configuration values out of a Python object. This is handy for a few weird situations. For example, you can use this to "bridge" Everett configuration with command line arguments. The argparse Namespace works fine here. Namespace (the Everett one--not the argparse one) is prefixed. So key "foo" in namespace "bar" is "foo_bar". For example:: import argparse from everett.manager import ConfigObjEnv, ConfigManager parser = argparse.ArgumentParser() parser.add_argument( "--debug", help="to debug or not to debug" ) parsed_vals = parser.parse_known_args()[0] config = ConfigManager([ ConfigObjEnv(parsed_vals) ]) print config("debug", parser=bool) Keys are not case-sensitive--everything is converted to lowercase before pulling it from the object. .. Note:: ConfigObjEnv has nothing to do with the library configobj. .. versionadded:: 0.6 """ def __init__(self, obj: Any, force_lower: bool = True): self.obj = obj def get( self, key: str, namespace: Optional[list[str]] = None ) -> Union[str, NoValue]: """Retrieve value for key.""" full_key = generate_uppercase_key(key, namespace) full_key = full_key.lower() logger.debug(f"Searching {self!r} for {full_key}") # Build a map of lowercase -> actual key obj_keys = { item.lower(): item for item in dir(self.obj) if not item.startswith("__") } if full_key in obj_keys: val = getattr(self.obj, obj_keys[full_key]) # If the value is None, then we're going to treat it as a non-valid # value. if val is not None: # This is goofy, but this allows people to specify arg parser # defaults, but do the right thing in Everett where everything # is a string until it's parsed. return str(val) return NO_VALUE def __repr__(self) -> str: return "" class ConfigDictEnv: """Source for pulling configuration out of a dict. This is handy for testing. You might also use it if you wanted to move all your defaults values into one centralized place. Keys are prefixed by namespaces and the whole thing is uppercased. For example, namespace "bar" for key "foo" becomes ``BAR_FOO`` in the dict. For example:: from everett.manager import ConfigDictEnv, ConfigManager config = ConfigManager([ ConfigDictEnv({ "FOO_BAR": "someval", "BAT": "1", }) ]) Keys are not case sensitive. This also works:: from everett.manager import ConfigDictEnv, ConfigManager config = ConfigManager([ ConfigDictEnv({ "foo_bar": "someval", "bat": "1", }) ]) print config("foo_bar") print config("FOO_BAR") print config.with_namespace("foo")("bar") Also, ``ConfigManager`` has a convenience classmethod for creating a ``ConfigManager`` with just a dict environment:: from everett.manager import ConfigManager config = ConfigManager.from_dict({ "FOO_BAR": "bat" }) .. versionchanged:: 0.3 Keys are no longer case-sensitive. """ def __init__(self, cfg: dict): self.cfg = {key.upper(): val for key, val in cfg.items()} def get( self, key: str, namespace: Optional[list[str]] = None ) -> Union[str, NoValue]: """Retrieve value for key.""" full_key = generate_uppercase_key(key, namespace) logger.debug(f"Searching {self!r} for {full_key}") return get_key_from_envs(self.cfg, full_key) def __repr__(self) -> str: return f"" class ConfigEnvFileEnv: """Source for pulling configuration out of ``.env`` files. This source lets you specify configuration in an .env file. This is useful for local development when in production you use values in environment variables. Keys are prefixed by namespaces and the whole thing is uppercased. For example, key "foo" will be ``FOO`` in the file. For example, namespace "bar" for key "foo" becomes ``BAR_FOO`` in the file. Key and namespace can consist of alphanumeric characters and ``_``. To use, instantiate and toss in the source list:: from everett.manager import ConfigEnvFileEnv, ConfigManager config = ConfigManager([ ConfigEnvFileEnv('.env') ]) For multiple paths:: from everett.manager import ConfigEnvFileEnv, ConfigManager config = ConfigManager([ ConfigEnvFileEnv([ '.env', 'config/prod.env' ]) ]) Here's an example .env file:: DEBUG=true # secrets SECRET_KEY=ou812 # database setup DB_HOST=localhost DB_PORT=5432 # CSP reporting CSP_SCRIPT_SRC="'self' www.googletagmanager.com" """ def __init__(self, possible_paths: Union[str, list[str]]): self.data = {} self.path = None possible_paths = listify(possible_paths) for path in possible_paths: if not path: continue path = os.path.abspath(os.path.expanduser(path.strip())) if path and os.path.isfile(path): self.path = path with open(path) as envfile: self.data = parse_env_file(envfile) break def get( self, key: str, namespace: Optional[list[str]] = None ) -> Union[str, NoValue]: """Retrieve value for key.""" full_key = generate_uppercase_key(key, namespace) logger.debug(f"Searching {self!r} for {full_key}") return get_key_from_envs(self.data, full_key) def __repr__(self) -> str: return f"" class ConfigOSEnv: """Source for pulling configuration out of the environment. This source lets you specify configuration in the environment. This is useful for infrastructure related configuration like usernames and ports and secret configuration like passwords. Keys are prefixed by namespaces and the whole thing is uppercased. For example, key "foo" will be ``FOO`` in the environment. For example, namespace "bar" for key "foo" becomes ``BAR_FOO`` in the environment. Key and namespace can consist of alphanumeric characters and ``_``. .. Note:: Unlike other config environments, this one is case sensitive in that keys defined in the environment **must** be all uppercase. For example, these are good:: FOO=bar FOO_BAR=bar FOO_BAR1=bar This is bad:: foo=bar To use, instantiate and toss in the source list:: from everett.manager import ConfigOSEnv, ConfigManager config = ConfigManager([ ConfigOSEnv() ]) """ def get( self, key: str, namespace: Optional[list[str]] = None ) -> Union[str, NoValue]: """Retrieve value for key.""" full_key = generate_uppercase_key(key, namespace) logger.debug(f"Searching {self!r} for {full_key}") return get_key_from_envs(os.environ, full_key) def __repr__(self) -> str: return "" def _get_component_name(component: Any) -> str: if not inspect.isclass(component): cls = component.__class__ else: cls = component return cls.__module__ + "." + cls.__name__ def get_runtime_config( config: "ConfigManager", component: Any, traverse: Callable = traverse_tree, ) -> list[tuple[list[str], str, Any, Option]]: """Returns configuration specification and values for a component tree For example, if you had a tree of components instantiated, you could traverse the tree and log the configuration:: from everett.manager import ( ConfigManager, generate_uppercase_key, get_runtime_config, Option, parse_class, ) class App: class Config: debug = Option(default="False", parser=bool) reader = Option(parser=parse_class) writer = Option(parser=parse_class) def __init__(self, config): self.config = config.with_options(self) # App has a reader and a writer each of which has configuration # options self.reader = self.config("reader")(config.with_namespace("reader")) self.writer = self.config("writer")(config.with_namespace("writer")) class Reader: class Config: input_file = Option() def __init__(self, config): self.config = config.with_options(self) class Writer: class Config: output_file = Option() def __init__(self, config): self.config = config.with_options(self) cm = ConfigManager.from_dict( { # This specifies which reader component to use. Because we # specified this one, we need to define a READER_INPUT_FILE # value. "READER": "__main__.Reader", "READER_INPUT_FILE": "input.txt", # Same thing for the writer component. "WRITER": "__main__.Writer", "WRITER_OUTPUT_FILE": "output.txt", } ) my_app = App(cm) # This traverses the component tree starting with my_app and then # traversing .reader and .writer attributes. for namespace, key, value, option in get_runtime_config(cm, my_app): full_key = generate_uppercase_key(key, namespace) print(f"{full_key.upper()}={value or ''}") # This should print out: # DEBUG=False # READER=__main__.Reader # READER_INPUT_FILE=input.txt # WRITER=__main__.Writer # WRITER_OUTPUT_FILE=output.txt :param config: a configuration manager instance :param component: a component or tree of components :param traverse: the function for traversing the component tree; see :py:func:`everett.manager.traverse_tree` for signature :returns: a list of (namespace, key, value, option) tuples """ runtime_config = [] for namespace, key, option, obj in traverse(component): runtime_config.append( ( namespace, key, config.with_namespace(namespace).with_options(obj)( key, raise_error=False, raw_value=True ), option, ) ) return runtime_config class ConfigManager: """Manage multiple configuration environment layers.""" def __init__( self, environments: list[Any], doc: str = "", msg_builder: Callable = build_msg, with_override: bool = True, ): """Instantiate a ConfigManager. :param environments: list of configuration sources to look through in the order they should be looked through :param doc: help text printed to users when they encounter configuration errors .. versionadded:: 0.6 :param msg_builder: function that takes arguments and builds an exception message intended to be printed or conveyed to the user For example:: def build_msg(namespace, key, parser, msg="", option_doc="", config_doc=""): full_key = namespace or [] full_key = "_".join(full_key + [key]).upper() return ( f"{full_key} requires a value parseable by {qualname(parser)}\\n" + option_doc + "\\n" + config_doc + "\\n" ) :param with_override: whether or not to insert the special override environment used for testing as the first environment in the list of sources """ self.with_override = with_override if with_override: # Add ConfigOverrideEnv if it's not in the environments list already override = [ env for env in environments if isinstance(env, ConfigOverrideEnv) ] if not override: environments.insert(0, ConfigOverrideEnv()) self.envs = environments self.doc = doc self.msg_builder = msg_builder self.namespace: list[str] = [] self.bound_component: Any = None self.bound_component_prefix: list[str] = [] self.bound_component_options: Mapping[str, Any] = {} self.original_manager = self @classmethod def basic_config(cls, env_file: str = ".env", doc: str = "") -> "ConfigManager": """Return a basic ConfigManager. This sets up a ConfigManager that will look for configuration in this order: 1. environment 2. specified ``env_file`` defaulting to ``.env`` This is for a fast one-line opinionated setup. Example:: from everett.manager import ConfigManager config = ConfigManager.basic_config() This is shorthand for:: config = ConfigManager( environments=[ ConfigOSEnv(), ConfigEnvFileEnv(['.env']) ] ) :param env_file: the name of the env file to use :param doc: help text printed to users when they encounter configuration errors :returns: a :py:class:`everett.manager.ConfigManager` """ return cls(environments=[ConfigOSEnv(), ConfigEnvFileEnv([env_file])], doc=doc) @classmethod def from_dict(cls, dict_config: dict) -> "ConfigManager": """Create a ConfigManager with specified configuration as a Python dict. This is shorthand for:: config = ConfigManager([ConfigDictEnv(dict_config)]) This is handy for writing tests for the app you're using Everett in. :param dict_config: Python dict holding the configuration for this manager :returns: ConfigManager with specified configuration .. versionadded:: 0.3 """ return cls([ConfigDictEnv(dict_config)]) def get_bound_component(self) -> Any: """Retrieve the bound component for this config object. :returns: component or None """ return self.bound_component def get_namespace(self) -> list[str]: """Retrieve the complete namespace for this config object. :returns: namespace as a list of strings """ return self.namespace def _get_base_config(self) -> "ConfigManager": return self.original_manager def clone(self) -> "ConfigManager": my_clone = ConfigManager( environments=list(self.envs), doc=self.doc, msg_builder=self.msg_builder, with_override=self.with_override, ) my_clone.namespace = list(self.namespace) my_clone.bound_component = self.bound_component my_clone.bound_component_prefix = [] my_clone.bound_component_options = self.bound_component_options my_clone.original_manager = self.original_manager return my_clone def with_namespace(self, namespace: Union[list[str], str]) -> "ConfigManager": """Apply a namespace to this configuration. Namespaces accumulate as you add them. :param namepace: namespace as a string or list of strings :returns: a clone of the ConfigManager instance with the namespace applied """ namespace = listify(namespace) if not namespace: return self my_clone = self.clone() if my_clone.bound_component: my_clone.bound_component_prefix.extend(namespace) else: my_clone.namespace.extend(namespace) return my_clone def with_options(self, component: Any) -> "ConfigManager": """Apply options component options to this configuration. :param component: the instance or class with a Config to bind this ConfigManager to :returns: a clone of the ConfigManager instance bound to specified component """ # If this is an instance, get the class if not inspect.isclass(component): component = component.__class__ options = get_config_for_class(component) # NOTE(willkg): if the component has no options, then there's nothing # to bind to if not options: return self my_clone = self.clone() my_clone.bound_component = component my_clone.bound_component_prefix = [] my_clone.bound_component_options = options # IF there's a bound component with a prefix, then it means someone is doing # something like: # # config = config.with_options(Comp).with_namespace("foo").with_options(SubComp) # # In that case, we want the namespace "foo" to be part of the namespace # and not part of the key prefix for SubComp. if self.bound_component_prefix: my_clone.namespace.extend(self.bound_component_prefix) return my_clone def __call__( self, key: str, namespace: Union[list[str], str, None] = None, default: Union[str, NoValue] = NO_VALUE, default_if_empty: bool = True, alternate_keys: Optional[list[str]] = None, doc: str = "", parser: Callable = str, raise_error: bool = True, raw_value: bool = False, ) -> Any: """Return a parsed value from the environment. :param key: the key to look up :param namespace: the namespace for the key--different environments use this differently :param default: the default value (if any); this must be a string that is parseable by the specified parser; if no default is provided, this will raise an error or return ``everett.NO_VALUE`` depending on the value of ``raise_error`` If this ConfigManager is bound to a component, the default will be the default of the option in the bound component configuration. :param default_if_empty: if True, treat empty string values as a non-value and return the specified default :param alternate_keys: the list of alternate keys to look up; supports a ``root:`` key prefix which will cause this to look at the configuration root rather than the current namespace If this ConfigManager is bound to a component, the alternate_keys will be the alternate_keys of the option in the bound component configuration. .. versionadded:: 0.3 :param doc: documentation for this config option If this ConfigManager is bound to a component, the doc will be the doc of the option in the bound component configuration. .. versionadded:: 0.6 :param parser: the parser for converting this value to a Python object If this ConfigManager is bound to a component, the parser will be the parser of the option in the bound component configuration. :param raise_error: True if you want a lack of value to raise a ``everett.ConfigurationError`` :param raw_value: True if you want the raw unparsed value, False otherwise :raises everett.ConfigurationMissingError: if the required bit of configuration is missing from all the environments :raises everett.InvalidKeyError: if the configuration key doesn't exist for that component :raises everett.InvalidValueError: if the configuration value is invalid in some way (not an integer, not a bool, etc) .. Note:: The default value should **always** be a string that is parseable by the parser. This simplifies thinking about values since **all** values are strings that are parsed by the parser rather than default values do one thing and non-default values doa nother. Further, it simplifies documentation for the user since the default value is an example value. The parser can be any callable that takes a string value and returns a parsed value. """ if not (default is NO_VALUE or isinstance(default, str)): raise ConfigurationError(f"default value {default!r} is not a string") # If we have a bound component, then the "namespace" is a key prefix, # so do that. Otherwise it's a namespace. if self.bound_component: key = "_".join( listify(self.bound_component_prefix) + listify(namespace) + [key] ) namespace = self.namespace else: namespace = self.namespace + listify(namespace) # If this is a bound config, then apply everything to that if self.bound_component: try: option, cls = self.bound_component_options[key] except KeyError as exc: if raise_error: raise InvalidKeyError( f"{key!r} is not a valid key for this component" ) from exc return None default = option.default alternate_keys = option.alternate_keys doc = option.doc parser = option.parser if raw_value: # If we're returning raw values, then we can just use str which is # a no-op. parser = str else: parser = get_parser(parser) # Go through all possible keys all_keys = [key] if alternate_keys: all_keys = all_keys + alternate_keys for possible_key in all_keys: if possible_key.startswith("root:"): # If this is a root-anchored key, we drop the namespace. possible_key = possible_key[5:] use_namespace = None else: use_namespace = namespace logger.debug(f"Looking up key: {possible_key}, namespace: {use_namespace}") # Go through environments in reverse order for env in self.envs: val = env.get(possible_key, use_namespace) # If the value is the empty string and default_if_empty is # True, treat it as a non-value if val == "" and default_if_empty: val = NO_VALUE if val is not NO_VALUE: try: parsed_val = parser(val) logger.debug(f"Returning raw: {val!r}, parsed: {parsed_val!r}") return parsed_val except ConfigurationError: # Re-raise ConfigurationError and friends since that's # what we want to be raising. raise except Exception as exc: exc_type, exc_value, exc_traceback = sys.exc_info() exc_type_name = exc_type.__name__ if exc_type else "None" msg = self.msg_builder( namespace=use_namespace, key=key, parser=parser, msg=f"{exc_type_name}: {exc_value}", option_doc=doc, config_doc=self.doc, ) raise InvalidValueError(msg, namespace, key, parser) from exc # Return the default if there is one if default is not NO_VALUE: try: parsed_val = parser(default) logger.debug( f"Returning default raw: {default!r}, parsed: {parsed_val!r}" ) return parsed_val except ConfigurationError: # Re-raise ConfigurationError and friends since that's # what we want to be raising. raise except Exception as exc: # FIXME(willkg): This is a programmer error--not a user # configuration error. We might want to denote that better. exc_type, exc_value, exc_traceback = sys.exc_info() exc_type_name = exc_type.__name__ if exc_type else "None" msg = self.msg_builder( namespace=use_namespace, key=key, parser=parser, msg=f"{exc_type_name}: {exc_value} (default value)", option_doc=doc, config_doc=self.doc, ) raise InvalidValueError(msg, namespace, key, parser) from exc # No value specified and no default, so raise an error to the user if raise_error: msg = self.msg_builder( namespace=use_namespace, key=key, parser=parser, option_doc=doc, config_doc=self.doc, ) raise ConfigurationMissingError(msg, namespace, key, parser) logger.debug("Found nothing--returning NO_VALUE") # Otherwise return NO_VALUE return NO_VALUE def raise_configuration_error(self, msg: str) -> None: """Convenience function for raising configuration errors. This is helpful for situations where you need to do additional checking of configuration values and need to raise a configuration error for the user that includes the configuration documentation. For example:: from everett.manager import ConfigManager config = ConfigManager.basic_config() host = config("host") port = config("port") if host is None or port is None: config.raise_configuration_error( "Both HOST and PORT must be specified." ) :param msg: the configuration error message """ msg = self.msg_builder( namespace=None, key=None, parser=None, msg=msg, option_doc=None, config_doc=self.doc, ) raise ConfigurationError(msg) def __repr__(self) -> str: if self.bound_component: name = _get_component_name(self.bound_component) return f"" else: return f"" # This is a stack of overrides to be examined in reverse order _CONFIG_OVERRIDE = [] class ConfigOverride: """Handle contexts and decoration for overriding config in testing.""" def __init__(self, **cfg: str): self._cfg = cfg def push_config(self) -> None: """Push ``self._cfg`` as a config layer onto the stack.""" _CONFIG_OVERRIDE.append(self._cfg) def pop_config(self) -> None: """Pop a config layer off. :raises IndexError: If there are no layers to pop off """ _CONFIG_OVERRIDE.pop() def __enter__(self) -> None: self.push_config() def __exit__( self, exc_type: Optional[type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: self.pop_config() def decorate(self, fun: Callable) -> Callable: """Decorate a function for overriding configuration.""" @wraps(fun) def _decorated(*args: Any, **kwargs: Any) -> Any: # Push the config, run the function and pop it afterwards. self.push_config() try: return fun(*args, **kwargs) finally: self.pop_config() return _decorated def __call__(self, class_or_fun: Callable) -> Callable: if inspect.isclass(class_or_fun): # If class_or_fun is a class, decorate all of its methods # that start with 'test'. for attr in class_or_fun.__dict__.keys(): prop = getattr(class_or_fun, attr) if attr.startswith("test") and callable(prop): setattr(class_or_fun, attr, self.decorate(prop)) return class_or_fun else: return self.decorate(class_or_fun) def config_override(**cfg: str) -> ConfigOverride: """Allow you to override config for writing tests. This can be used as a class decorator:: @config_override(FOO="bar", BAZ="bat") class FooTestClass(object): ... This can be used as a function decorator:: @config_override(FOO="bar") def test_foo(): ... This can also be used as a context manager:: def test_foo(): with config_override(FOO="bar"): ... """ return ConfigOverride(**cfg) ================================================ FILE: src/everett/sphinxext.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Sphinx extension for auto-documenting components with configuration. To use this, you must install the optional requirements:: $ pip install 'everett[sphinx]' """ import ast from importlib import import_module import re import textwrap from typing import ( TYPE_CHECKING, Any, Optional, Union, ) from collections.abc import Generator from docutils import nodes from docutils.parsers.rst import Directive, directives from docutils.statemachine import ViewList, StringList from sphinx import addnodes from sphinx.addnodes import desc_signature, pending_xref from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.locale import _ as gettext from sphinx.roles import XRefRole from sphinx.util import ws_re from sphinx.util import logging from sphinx.util.docfields import Field from sphinx.util.docstrings import prepare_docstring from sphinx.util.nodes import make_refnode from everett import NO_VALUE, __version__ from everett.manager import qualname, get_config_for_class if TYPE_CHECKING: from sphinx.builders import Builder from sphinx.environment import BuildEnvironment LOGGER = logging.getLogger(__name__) def split_clspath(clspath: str) -> list[str]: """Split clspath into module and class names. Note: This is a really simplistic implementation. """ return clspath.rsplit(".", 1) def get_module_and_objpath(path: str) -> Any: """Given a path, imports the module part of the path and returns the module and the rest of the path. :arg clspath: a "a.b.c.Class" style path :returns: "a.b.c" module and "Class" """ module = None parts = path.split(".") # Figure out the module for i in range(len(parts)): modpath = parts[:i] objpath = parts[i:] if not modpath: continue try: module = import_module(".".join(modpath)) except ImportError: break return module, ".".join(objpath) def import_class(clspath: str) -> Any: """Given a clspath, returns the class. Note: This is a really simplistic implementation. :arg clspath: a "a.b.c.Class" style path :returns: the Class """ module, objpath = get_module_and_objpath(clspath) if module is None: raise ValueError(f"{clspath!r} does not point to a valid thing") obj = module for part in objpath.split(","): obj = getattr(obj, part) return obj def upper_lower_none(arg: Optional[str]) -> Union[str, None]: """Validate arg value as "upper", "lower", or None.""" if not arg: return arg arg = arg.strip().lower() if arg in ["upper", "lower"]: return arg raise ValueError('argument must be "upper", "lower" or None') class EverettOption(ObjectDescription): """An Everett config option.""" indextemplate = "everett option; %s" option_spec = { # This is the parser for the option "parser": directives.unchanged_required, # The default for this option; no value (not NO_VALUE--that's different) is # treated as an empty string "default": directives.unchanged_required, # Whether or not this option is required "required": directives.flag, } def handle_signature(self, sig: str, signode: desc_signature) -> str: signode.clear() signode += addnodes.desc_name(sig, sig) name = ws_re.sub(" ", sig) return name def add_target_and_index( self, name: str, sig: str, signode: desc_signature ) -> None: ref = self.env.ref_context.get("everett:component") if ref: targetname = f"{self.objtype}-{ref}.{name}" # If this is in a component, we change the name to include the # component name name = f"{ref}.{name}" else: targetname = f"{self.objtype}-{name}" if targetname not in self.state.document.ids: signode["names"].append(targetname) signode["ids"].append(targetname) signode["first"] = not self.names self.state.document.note_explicit_target(signode) objects = self.env.domaindata["everett"]["objects"] key = (self.objtype, name) if key in objects: self.state_machine.reporter.warning( f"duplicate description of {self.objtype} {name!r}, " + f"other instance in {self.env.doc2path(objects[key][0])}", line=self.lineno, ) objects[key] = (self.env.docname, targetname) indextext = gettext("%s (component)") % name if self.indexnode is not None: self.indexnode["entries"].append( ("single", indextext, targetname, "", None) ) def transform_content(self, contentnode: addnodes.desc_content) -> None: # We want to insert some stuff before the content lines = StringList() sourcename = "everett option" parser = self.options.get("parser", "str") default = self.options.get("default") is_required = ("required" in self.options) or (default is None) required = "Yes" if is_required else "No" lines.append(f":Parser: *{parser}*", sourcename) if default is not None: lines.append(f":Default: {default}", sourcename) lines.append(f":Required: {required}", sourcename) lines.append("", sourcename) node = nodes.paragraph() node.document = self.state.document self.state.nested_parse(lines, 0, node) # Insert our new nodes before the rest of the content contentnode.children = node.children + contentnode.children class EverettComponent(ObjectDescription): """Description of an Everett component.""" doc_field_types = [ Field( "options", names=("option",), label=gettext("Options"), rolename="option", ) ] allow_nesting = False # FIXME(willkg): What's the signode here? def handle_signature(self, sig: str, signode: Any) -> str: """Create a signature for this thing.""" if sig != "Configuration": signode.clear() # Add "component" which is the type of this thing signode += addnodes.desc_annotation("component ", "component ") if "." in sig: modname, clsname = sig.rsplit(".", 1) else: modname, clsname = "", sig # If there's a module name, then we add the module if modname: signode += addnodes.desc_addname(modname + ".", modname + ".") # Add the class name signode += addnodes.desc_name(clsname, clsname) else: # Add just "Configuration" signode += addnodes.desc_name(sig, sig) return sig def add_target_and_index( self, name: str, sig: str, signode: desc_signature ) -> None: """Add a target and index for this thing.""" targetname = f"{self.objtype}-{name}" if targetname not in self.state.document.ids: signode["names"].append(targetname) signode["ids"].append(targetname) signode["first"] = not self.names self.state.document.note_explicit_target(signode) objects = self.env.domaindata["everett"]["objects"] key = (self.objtype, name) if key in objects: self.state_machine.reporter.warning( f"duplicate description of {self.objtype} {name!r}, " + f"other instance in {self.env.doc2path(objects[key][0])}", line=self.lineno, ) objects[key] = (self.env.docname, targetname) indextext = gettext("%s (component)") % name if self.indexnode is not None: self.indexnode["entries"].append( ("single", indextext, targetname, "", None) ) def before_content(self) -> None: if self.names: self.env.ref_context["everett:component"] = self.names[-1] def after_content(self) -> None: self.env.ref_context["everett:component"] = None class EverettDomain(Domain): """Everett domain for component configuration.""" name = "everett" label = "Everett" object_types = { "component": ObjType(gettext("component"), "component"), "option": ObjType(gettext("option"), "option"), } directives = { "component": EverettComponent, "option": EverettOption, } roles = { "component": XRefRole(), "option": XRefRole(), } initial_data: dict[str, dict] = { # (typ, clspath) -> sphinx document name "objects": {} } @property def objects(self) -> dict[tuple[str, str], tuple[str, str]]: return self.data.setdefault("objects", {}) def clear_doc(self, docname: str) -> None: key: Any = None for key, val in list(self.objects.items()): if val[0] == docname: del self.objects[key] # FIXME(willkg): What's the value in otherdata dict? def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: for key, val in otherdata["objects"].items(): if val[0] in docnames: self.objects[key] = val def resolve_xref( self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: nodes.Element, ) -> Optional[nodes.Element]: objtypes = self.objtypes_for_role(typ) or [] for objtype in objtypes: if (objtype, target) in self.objects: docname, labelid = self.objects[objtype, target] break else: docname, labelid = "", "" if docname: return make_refnode(builder, fromdocname, docname, labelid, contnode) return None class ConfigDirective(Directive): """Base class for generating configuration""" def add_line(self, line: str, source: str, *lineno: int) -> None: """Add a line to the result""" self.result.append(line, source, *lineno) # NOTE(willkg): This makes figuring out issues easier. Leaving it here # for future me. # if line.strip(): # print(f">>> {line} [{source} {lineno}]") # else: # print(">>> ") def generate_docs( self, component_name: str, component_index: str, docstring: str, sourcename: str, option_data: list[dict], more_content: Any, ) -> None: indent = " " # Add the classname or 'Configuration' self.add_line(".. everett:component:: %s" % component_name, sourcename) self.add_line("", sourcename) # Add the docstring if there is one and if show-docstring if "show-docstring" in self.options and docstring: docstringlines = prepare_docstring(docstring) for i, line in enumerate(docstringlines): self.add_line(indent + line, sourcename, i) self.add_line("", "") # Add content from the directive if there was any if more_content: for line, src in zip(more_content.data, more_content.items, strict=True): self.add_line(indent + line, src[0], src[1]) self.add_line("", "") if "show-table" in self.options and option_data: self.add_line(indent + "Configuration summary:", sourcename) self.add_line("", sourcename) # Build a table of metric items table: list[list[str]] = [] table.append(["Setting", "Parser", "Required?"]) for option_item in option_data: ref = f"{component_name}.{option_item['key']}" table.append( [ f":everett:option:`{option_item['key']} <{ref}>`", f"*{option_item['parser']}*", "Yes" if option_item["default"] is NO_VALUE else "", ] ) for line in build_table(table): self.add_line(indent + line, sourcename) self.add_line("", sourcename) self.add_line(indent + "Configuration options:", sourcename) self.add_line("", sourcename) sourcename = "class definition" if option_data: # List the options and details for option_item in option_data: key = option_item["key"] self.add_line(f"{indent}.. everett:option:: {key}", sourcename) self.add_line( f"{indent} :parser: {option_item['parser']}", sourcename ) if option_item["default"] is not NO_VALUE: self.add_line( f'{indent} :default: "{option_item["default"]}"', sourcename ) else: self.add_line(f"{indent} :required:", sourcename) self.add_line("", sourcename) doc = option_item["doc"] for doc_line in doc.splitlines(): self.add_line(f"{indent} {doc_line}", sourcename) self.add_line("", sourcename) else: # There are no options self.add_line(f"{indent}No configuration options.", sourcename) self.add_line("", sourcename) class AutoComponentConfigDirective(ConfigDirective): """Directive for documenting configuration for an Everett component.""" has_content = True required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False option_spec = { # Whether or not to show the class docstring--if None, don't show the # docstring, if empty string use __doc__, otherwise use the value of # the attribute on the class "show-docstring": directives.unchanged, # Whether or not to hide the class name "hide-name": directives.flag, # Prepend a specified namespace "namespace": directives.unchanged, # Render keys in specified case "case": upper_lower_none, # Whether or not to show a table "show-table": directives.flag, } def extract_configuration( self, obj: Any, namespace: Optional[str] = None, case: Optional[str] = None, ) -> list[dict]: """Extracts configuration values from list of Everett configuration options :param obj: object/class to extract configuration from :param namespace: namespace if any that these options are in :param case: None, "upper", or "lower" for converting the name :returns: list of dicts each representing an option """ config = get_config_for_class(obj) options: list[dict] = [] # Go through options and figure out relevant information for key, (option, _) in config.items(): if namespace: namespaced_key = namespace + "_" + key else: namespaced_key = key if case == "upper": namespaced_key = namespaced_key.upper() elif case == "lower": namespaced_key = namespaced_key.lower() options.append( { "key": namespaced_key, "default": option.default, "parser": qualname(option.parser), "doc": option.doc, "meta": {}, } ) return options def run(self) -> list[nodes.Node]: self.reporter = self.state.document.reporter self.result = ViewList() clspath = self.arguments[0] obj = import_class(clspath) sourcename = "configuration of %s" % clspath option_data = self.extract_configuration( obj=obj, namespace=self.options.get("namespace"), case=self.options.get("case"), ) if "hide-name" not in self.options: modname, clsname = split_clspath(clspath) component_name = clspath component_index = clsname else: component_name = "Configuration" component_index = "Configuration" # Add the docstring if there is one and if show-docstring if "show-docstring" in self.options: docstring_attr = self.options["show-docstring"] or "__doc__" docstring = getattr(obj, docstring_attr, "") else: docstring = "" self.generate_docs( component_name=component_name, component_index=component_index, docstring=docstring, sourcename=sourcename, option_data=option_data, more_content=self.content, ) if not self.result: return [] node = nodes.paragraph() node.document = self.state.document self.state.nested_parse(self.result, 0, node) return node.children SETTING_RE = re.compile(r"^[A-Z_]+$") def build_table(table: list[list[str]]) -> list[str]: """Generates reST for a table. :param table: a 2d array of rows and columns :returns: list of strings """ output: list[str] = [] col_size = [0] * len(table[0]) for row in table: for i, col in enumerate(row): col_size[i] = max(col_size[i], len(col)) col_size = [width + 2 for width in col_size] # Build header output.append(" ".join("=" * width for width in col_size)) output.append( " ".join( header + (" " * (width - len(header))) for header, width in zip(table[0], col_size, strict=True) ) ) output.append(" ".join("=" * width for width in col_size)) # Iterate through rows for row in table[1:]: output.append( " ".join( col + (" " * (width - len(col))) for col, width in zip(row, col_size, strict=True) ) ) output.append(" ".join("=" * width for width in col_size)) return output class AutoModuleConfigDirective(ConfigDirective): """Directive for documenting configuration for a module.""" has_content = True # path/to/module.py variablename required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False option_spec = { # Whether or not to show the class docstring--if None, don't show the # docstring, if empty string use __doc__, otherwise use the value of # the attribute on the class "show-docstring": directives.unchanged, # Whether or not to hide the name "hide-name": directives.flag, # Prepend a specified namespace "namespace": directives.unchanged, # Render keys in specified case "case": upper_lower_none, # Whether or not to show a table "show-table": directives.flag, } def _walk_ast(self, tree: ast.AST) -> Generator[ast.AST, None, None]: """Walks an AST returning Assign nodes :param tree: the tree to walk :returns: generator of Assign nodes """ for node in ast.walk(tree): if isinstance(node, (ast.Assign, ast.Dict)): yield node def extract_configuration( self, filepath: str, variable_name: str, namespace: Optional[str] = None, case: Optional[str] = None, ) -> list[dict]: """Extracts configuration values from a module at filepath :param filepath: the filepath to parse configuration from :param variable_name: the ConfigurationManager variable name :param namespace: namespace if any that these options are in :param case: None, "upper", or "lower" for converting the name :returns: list of dicts each representing an option """ with open(filepath) as fp: source = fp.read() tree = ast.parse(source=source, filename=filepath, mode="exec") config_nodes = [] for node in self._walk_ast(tree): if isinstance(node, ast.Assign): # Covers: # # SOMESETTING = _config("option", default="foo", ...) if ( len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) and SETTING_RE.match(node.targets[0].id) and isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) and node.value.func.id == variable_name ): config_nodes.append((node.targets[0].id, node.value)) elif isinstance(node, ast.Dict): # Covers: # # SOMESETTING = { # "NAME": _config("option", default="foo", ...), # "NAME2": _config("option2", default="foo", ...), # } for key, val in zip(node.keys, node.values, strict=True): if ( isinstance(key, ast.Constant) and isinstance(val, ast.Call) and isinstance(val.func, ast.Name) and val.func.id == variable_name ): config_nodes.append((str(key.value), val)) CONFIG_ARGS = [ "key", "default", "parser", "doc", "meta", ] def extract_value(source: str, val: ast.AST) -> tuple[str, str]: """Returns (category, value)""" if isinstance(val, ast.Constant): return "constant", str(val.value) if isinstance(val, ast.Name): return "name", val.id if isinstance(val, ast.BinOp) and isinstance(val.op, ast.Add): _, left = extract_value(source, val.left) _, right = extract_value(source, val.right) return "binop", left + right return "unknown", ast.get_source_segment(source, val) or "?" # Using a dict here avoids the case where configuration options are # defined multiple times configuration = {} for name, node in config_nodes: args: dict[str, Any] = { "key": name, "default": NO_VALUE, "parser": "str", "doc": "", "meta": {}, } for i, arg in enumerate(node.args): cat, value = extract_value(source, arg) # NOTE(willkg): we're dropping the cat here; but we might want # to do something with the category in the future, so I'm # leaving the figuring in for now args[CONFIG_ARGS[i]] = value for keyword in node.keywords: # NOTE(willkg): mypy thinks this can be None for some reason, # but I'm not sure why. If it is None, we should skip it. if keyword.arg is None: continue cat, value = extract_value(source, keyword.value) if keyword.arg == "doc": value = textwrap.dedent(value) # NOTE(willkg): we're dropping the cat here; but we might want # to do something with the category in the future, so I'm # leaving the figuring in for now args[keyword.arg] = value key = args["key"] if namespace: namespaced_key = f"{namespace}_{key}" else: namespaced_key = str(key) if case == "upper": namespaced_key = namespaced_key.upper() elif case == "lower": namespaced_key = namespaced_key.lower() args["key"] = namespaced_key configuration[name] = args return list(configuration.values()) def run(self) -> list[nodes.Node]: self.reporter = self.state.document.reporter self.result = ViewList() clspath = self.arguments[0] module, objpath = get_module_and_objpath(clspath) if module is None: raise ValueError(f"{clspath!r} does not point to a valid thing") filepath = module.__file__ variable_name = objpath if not variable_name: raise ValueError("Variable in module is unknown") sourcename = "configuration of %s" % clspath option_data = self.extract_configuration( filepath=filepath, variable_name=variable_name, namespace=self.options.get("namespace"), case=self.options.get("case"), ) if "hide-name" not in self.options: modname, clsname = split_clspath(clspath) component_name = clspath component_index = clsname else: component_name = "Configuration" component_index = "Configuration" # Add the docstring if there is one and if show-docstring if "show-docstring" in self.options: obj = module docstring_attr = self.options["show-docstring"] or "__doc__" docstring = getattr(obj, docstring_attr, "") else: docstring = "" self.generate_docs( component_name=component_name, component_index=component_index, docstring=docstring, sourcename=sourcename, option_data=option_data, more_content=self.content, ) if not self.result: return [] node = nodes.paragraph() node.document = self.state.document self.state.nested_parse(self.result, 0, node) return node.children # FIXME(willkg): this takes a Sphinx app def setup(app: Any) -> dict[str, Any]: """Register domain and directive in Sphinx.""" app.add_domain(EverettDomain) app.add_directive("autocomponentconfig", AutoComponentConfigDirective) app.add_directive("automoduleconfig", AutoModuleConfigDirective) return { "version": __version__, "parallel_read_safe": True, "parallel_write_safe": True, } ================================================ FILE: tests/basic_component_config.py ================================================ """Basic component config.""" from everett.manager import ListOf, Option, parse_class class ComponentBasic: """Basic component. Multiple lines. """ HELP = "Help attribute value." class Config: user = Option() class ComponentNoOptions: """Basic component with no options.""" class Config: pass class ComponentSubclass(ComponentBasic): """A different docstring.""" class ComponentOptionDefault: class Config: user = Option(default="ou812") class ComponentOptionDoc: class Config: user = Option(doc="ou812") class ComponentOptionDocMultiline: class Config: user = Option(doc="ou812") password = Option(doc="First ``paragraph``.\n\nSecond paragraph.") class ComponentOptionDocDefault: class Config: user = Option(doc="This is some docs.", default="ou812") class Foo: @classmethod def parse_foo_class(cls, value): pass def parse_foo_instance(self, value): pass class ComponentOptionParser: class Config: user_builtin = Option(parser=int) user_parse_class = Option(parser=parse_class) user_listof = Option(parser=ListOf(str)) user_class_method = Option(parser=Foo.parse_foo_class) user_instance_method = Option(parser=Foo().parse_foo_instance) class ComponentWithDocstring: """This component is the best. The best! """ class Config: user = Option() class ComponentDocstringOtherAttribute: """Programming-focused help""" __everett_help__ = """ User-focused help """ class Config: user = Option() ================================================ FILE: tests/basic_module_config.py ================================================ """Basic module config.""" from everett.manager import ConfigManager _config = ConfigManager.from_dict( {"debug": "False", "logging_level": "INFO", "password": "pwd", "fun": "0.0"} ) def parse_logging_level(s: str) -> str: if s not in ("CRITICAL", "WARNING", "INFO", "ERROR"): raise ValueError("invalid logging level value") return s DEBUG = _config(key="debug", parser=bool, default="False", doc="Debug mode.") LOGGING_LEVEL = _config(key="logging_level", parser=parse_logging_level, doc="Level.") PASSWORD = _config(key="password", doc="Password field.\n\nMust be provided.") FUN = _config(key="fun", parser=(int if 0 else float), doc="Woah.") CACHES = { "default": { "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", "LOCATION": _config( "cache_location", default="127.0.0.1:11211", doc="The location" ), } } LONG_DESC = _config( key="long_description", default="", doc=( "This configuration item has a really long description that spans " + "several lines so we can test runtime string concatenation.\n\n" + "Multiple lines should work, too." ), ) ================================================ FILE: tests/conftest.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import os import pytest @pytest.fixture def datadir(): return os.path.join(os.path.dirname(__file__), "data") ================================================ FILE: tests/data/config_test.ini ================================================ [main] foo = bar bar = test1,test2 [nsbaz] foo = bat [[nsbaz2]] foo = bat2 ================================================ FILE: tests/data/config_test_original.ini ================================================ [main] foo_original = original ================================================ FILE: tests/ext/test_inifile.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import os from everett import NO_VALUE from everett.ext.inifile import ConfigIniEnv class TestConfigIniEnv: def test_basic_usage(self, datadir): ini_filename = os.path.join(datadir, "config_test.ini") cie = ConfigIniEnv([ini_filename]) assert cie.get("foo") == "bar" assert cie.get("FOO") == "bar" assert cie.get("foo", namespace="nsbaz") == "bat" assert cie.get("foo", namespace=["nsbaz"]) == "bat" assert cie.get("foo", namespace=["nsbaz", "nsbaz2"]) == "bat2" cie = ConfigIniEnv(["/a/b/c/bogus/filename"]) assert cie.get("foo") == NO_VALUE def test_multiple_files(self, datadir): ini_filename = os.path.join(datadir, "config_test.ini") ini_filename_original = os.path.join(datadir, "config_test_original.ini") cie = ConfigIniEnv([ini_filename, ini_filename_original]) # Only the first found file is loaded, so foo_original does not exist assert cie.get("foo_original") == NO_VALUE cie = ConfigIniEnv([ini_filename_original]) # ... but it is there if only the original is loaded (safety check) assert cie.get("foo_original") == "original" def test_does_not_parse_lists(self, datadir): ini_filename = os.path.join(datadir, "config_test.ini") cie = ConfigIniEnv([ini_filename]) assert cie.get("bar") == "test1,test2" ================================================ FILE: tests/ext/test_yamlfile.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import pytest from everett import NO_VALUE, ConfigurationError from everett.ext.yamlfile import ConfigYamlEnv YAML_FLAT = """\ foo: "bar" bar: "test1,test2" nsbaz_foo: "bat" nsbaz_nsbaz2_foo: "bat2" """ YAML_FLAT_2 = """\ foo_original: "original" """ YAML_FLAT_CASE = """\ FOO: "bar" NSBAZ_Foo: "bat" """ YAML_NON_STRING_VALUES = """\ foo: 5 """ YAML_HIERARCHY = """\ foo: "bar" bar: "test1,test2" nsbaz: foo: "bat" nsbaz2: foo: "bat2" """ class TestConfigYamlEnv: def test_missing_file(self): cie = ConfigYamlEnv(["/a/b/c/bogus/filename"]) assert cie.get("foo") == NO_VALUE def test_flat(self, tmpdir): """Test flat specification works""" yaml_filename = tmpdir / "config.yaml" yaml_filename.write(YAML_FLAT) cie = ConfigYamlEnv([str(yaml_filename)]) assert cie.get("foo") == "bar" assert cie.get("foo", namespace="nsbaz") == "bat" assert cie.get("foo", namespace=["nsbaz"]) == "bat" assert cie.get("foo", namespace=["nsbaz", "nsbaz2"]) == "bat2" def test_flat_caps(self, tmpdir): """Test case-insensitive""" yaml_filename = tmpdir / "config.yaml" yaml_filename.write(YAML_FLAT) cie = ConfigYamlEnv([str(yaml_filename)]) assert cie.get("foo") == "bar" assert cie.get("FOO") == "bar" assert cie.get("Foo") == "bar" assert cie.get("foo", namespace="nsbaz") == "bat" assert cie.get("foo", namespace="NsBaz") == "bat" assert cie.get("FOO", namespace="NSBAZ") == "bat" def test_hierarchical(self, tmpdir): """Test hierarchical specification works""" yaml_filename = tmpdir / "config.yaml" yaml_filename.write(YAML_HIERARCHY) cie = ConfigYamlEnv([str(yaml_filename)]) assert cie.get("foo") == "bar" assert cie.get("foo", namespace="nsbaz") == "bat" assert cie.get("foo", namespace=["nsbaz"]) == "bat" assert cie.get("foo", namespace=["nsbaz", "nsbaz2"]) == "bat2" def test_multiple_files(self, tmpdir): """Test multiple files--uses first found""" yaml_filename = tmpdir / "config.yaml" yaml_filename.write(YAML_FLAT) yaml_filename_2 = tmpdir / "config2.yaml" yaml_filename_2.write(YAML_FLAT_2) cie = ConfigYamlEnv([str(yaml_filename), str(yaml_filename_2)]) # Only the first found file is loaded, so foo_original does not exist assert cie.get("foo_original") == NO_VALUE cie = ConfigYamlEnv([str(yaml_filename_2)]) # ... but it is there if only the original is loaded (safety check) assert cie.get("foo_original") == "original" def test_non_string_values(self, tmpdir): yaml_filename = tmpdir / "config.yaml" yaml_filename.write(YAML_NON_STRING_VALUES) with pytest.raises(ConfigurationError): ConfigYamlEnv([str(yaml_filename)]) ================================================ FILE: tests/simple_module_config.py ================================================ """Simple module config.""" from everett.manager import ConfigManager HELP = "Help attribute value." _config = ConfigManager.from_dict({"host": "localhost"}) HOST = _config(key="host", default="localhost", doc="The host.") ================================================ FILE: tests/test_manager.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import argparse import os import pytest from everett import ( ConfigurationError, ConfigurationMissingError, InvalidValueError, NO_VALUE, ) import everett.manager from everett.manager import ( ChoiceOf, ConfigDictEnv, ConfigEnvFileEnv, ConfigManager, ConfigObjEnv, ConfigOSEnv, config_override, generate_uppercase_key, get_config_for_class, get_key_from_envs, get_parser, get_runtime_config, listify, ListOf, Option, parse_bool, parse_class, parse_data_size, parse_env_file, parse_time_period, qualname, ) @pytest.mark.parametrize( "thing, expected", [ # built-in (str, "str"), # function in a module (qualname, "everett.manager.qualname"), # module (everett.manager, "everett.manager"), # class (ConfigManager, "everett.manager.ConfigManager"), # class method (ConfigManager.basic_config, "everett.manager.ConfigManager.basic_config"), # instance (ListOf(bool), ""), # instance (ChoiceOf(int, ["1", "10", "100"]), ""), # instance method (ConfigOSEnv().get, "everett.manager.ConfigOSEnv.get"), ], ) def test_qualname(thing, expected): assert qualname(thing) == expected def test_get_config_for_class(): """Verify that get_config_for_class works for a trivial class""" class Component: class Config: user = Option(doc="no help") options = get_config_for_class(Component) assert list(options.keys()) == ["user"] def test_get_config_for_class_complex_mro(): """Verify get_config_for_class with an MRO that has a diamond shape to it. The goal here is to make sure the C class has the right options from the right components and in the right order. """ class ComponentBase: pass class A(ComponentBase): class Config: a = Option(default="a") class B(ComponentBase): class Config: b = Option(default="b") bd = Option(default="b") class C(B, A): class Config: # Note: This overrides B's b. b = Option(default="c") c = Option(default="c") options = get_config_for_class(C) assert list( sorted([(key, opt.default) for key, (opt, cls) in options.items()]) ) == [ ("a", "a"), # from A ("b", "c"), # from C (overrides B's) ("bd", "b"), # from B ("c", "c"), # from C ] def test_no_value(): assert bool(NO_VALUE) is False assert NO_VALUE is not True assert str(NO_VALUE) == "NO_VALUE" def test_parse_bool_error(): with pytest.raises(ValueError): parse_bool("") @pytest.mark.parametrize( "data,expected", [ (None, []), ("", [""]), ([], []), ("foo", ["foo"]), (["foo"], ["foo"]), (["foo", "bar"], ["foo", "bar"]), ], ) def test_listify(data, expected): assert listify(data) == expected @pytest.mark.parametrize( "data", ["t", "true", "True", "TRUE", "y", "yes", "YES", "1", "on", "On", "ON"] ) def test_parse_bool_true(data): assert parse_bool(data) is True @pytest.mark.parametrize( "data", ["f", "false", "False", "FALSE", "n", "no", "No", "NO", "0", "off", "Off", "OFF"], ) def test_parse_bool_false(data): assert parse_bool(data) is False def test_parse_bool_with_config(): config = ConfigManager.from_dict({"foo": "bar"}) # Test key is there, but value is bad with pytest.raises(InvalidValueError) as excinfo: config("foo", parser=bool) assert str(excinfo.value) == ( "ValueError: 'bar' is not a valid bool value\n" "FOO requires a value parseable by everett.manager.parse_bool" ) # Test key is not there and default is bad with pytest.raises(InvalidValueError) as excinfo: config("phil", default="foo", parser=bool) assert str(excinfo.value) == ( "ValueError: 'foo' is not a valid bool value (default value)\n" "PHIL requires a value parseable by everett.manager.parse_bool" ) def test_parse_missing_class(): with pytest.raises(ImportError): parse_class("doesnotexist.class") with pytest.raises(ValueError): parse_class("hashlib.doesnotexist") def test_parse_class(): from hashlib import md5 assert parse_class("hashlib.md5") == md5 @pytest.mark.parametrize( "text, expected", [ ("0", 0), ("1_000", 1_000), # decimal ("10b", 10), (" 10b ", 10), ("1_000b", 1_000), ("1kb", 1_000), ("10kb", 10_000), ("5mb", 5_000_000), ("7gb", 7_000_000_000), ("32tb", 32_000_000_000_000), # binary ("10kib", 10_240), ("2MiB", 2_097_152), ("17gib", 18_253_611_008), ("4tib", 4_398_046_511_104), ], ) def test_parse_data_size(text, expected): assert parse_data_size(text) == expected @pytest.mark.parametrize("text", ["", "gb", "15yy"]) def test_parse_data_size_bad_values(text): with pytest.raises(ValueError): parse_data_size(text) @pytest.mark.parametrize( "text, expected", [ ("15", 15), ("1_000", 1_000), ("1s", 1), ("10m", 600), ("1_000m", 60_000), ("10m3s", 603), ("1d10m", 87_000), ("1w2d", 777_600), (" 1w 2d ", 777_600), ], ) def test_parse_time_period(text, expected): assert parse_time_period(text) == expected @pytest.mark.parametrize("text", ["", "m", "10j"]) def test_parse_time_period_bad_values(text): with pytest.raises(ValueError): parse_time_period(text) def test_parse_class_config(): config = ConfigManager.from_dict( {"foo_cls": "hashlib.doesnotexist", "bar_cls": "doesnotexist.class"} ) with pytest.raises(InvalidValueError) as exc_info: config("foo_cls", parser=parse_class) assert str(exc_info.value) == ( "ValueError: 'doesnotexist' is not a valid member of hashlib\n" "FOO_CLS requires a value parseable by everett.manager.parse_class" ) with pytest.raises(InvalidValueError) as exc_info: config("bar_cls", parser=parse_class) assert str(exc_info.value) == ( "ModuleNotFoundError: No module named 'doesnotexist'\n" "BAR_CLS requires a value parseable by everett.manager.parse_class" ) def test_get_parser(): assert get_parser(bool) == parse_bool assert get_parser(str) is str def foo(): pass assert get_parser(foo) == foo def test_ListOf(): assert ListOf(str)("") == [] assert ListOf(str)("foo") == ["foo"] assert ListOf(bool)("t,f") == [True, False] assert ListOf(int)("1,2,3") == [1, 2, 3] assert ListOf(str)("1 , 2, 3") == ["1", "2", "3"] assert ListOf(int, delimiter=":")("1:2") == [1, 2] assert ListOf(str)("a,,b") == ["a", "", "b"] def test_ListOf_error(): config = ConfigManager.from_dict({"bools": "t,f,badbool"}) with pytest.raises(InvalidValueError) as exc_info: config("bools", parser=ListOf(bool)) assert str(exc_info.value) == ( "ValueError: 'badbool' is not a valid bool value\n" "BOOLS requires a value parseable by " "" ) def test_ListOf_allow_empty_error(): config = ConfigManager.from_dict({"names": "bob,,alice"}) with pytest.raises(InvalidValueError) as exc_info: config("names", parser=ListOf(str, allow_empty=False)) assert str(exc_info.value) == ( "ValueError: 'bob,,alice' can not have empty values\n" "NAMES requires a value parseable by " "" ) def test_ChoiceOf(): # Supports any choice assert ChoiceOf(str, ["a", "b", "c"])("a") == "a" assert ChoiceOf(str, ["a", "b", "c"])("b") == "b" assert ChoiceOf(str, ["a", "b", "c"])("c") == "c" # Supports different parsers assert ChoiceOf(int, ["1", "2", "3"])("1") == 1 def test_ChoiceOf_bad_choices(): # Must provide choices with pytest.raises(ValueError) as exc_info: ChoiceOf(str, []) assert str(exc_info.value) == "choices [] must be a non-empty list of strings" # Must be a list of strings with pytest.raises(ValueError) as exc_info: ChoiceOf(str, [1, 2, 3]) assert ( str(exc_info.value) == "choices [1, 2, 3] must be a non-empty list of strings" ) def test_ChoiceOf_error(): # Value is the wrong case with pytest.raises(ValueError) as exc_info: ChoiceOf(str, ["A", "B", "C"])("c") assert str(exc_info.value) == "'c' is not a valid choice" # Value isn't a valid choice config = ConfigManager.from_dict({"cloud_provider": "foo"}) with pytest.raises(InvalidValueError) as exc_info: config("cloud_provider", parser=ChoiceOf(str, ["aws", "gcp"])) assert str(exc_info.value) == ( "ValueError: 'foo' is not a valid choice\n" "CLOUD_PROVIDER requires a value parseable by " ) class TestConfigObjEnv: def test_basic(self): class Namespace: pass obj = Namespace() setattr(obj, "foo", "bar") # noqa setattr(obj, "foo_baz", "bar") # noqa coe = ConfigObjEnv(obj) assert coe.get("foo") == "bar" assert coe.get("FOO") == "bar" assert coe.get("FOO_BAZ") == "bar" def test_with_argparse(self): parser = argparse.ArgumentParser() parser.add_argument("--debug", help="to debug or not to debug") parsed_vals = parser.parse_known_args([])[0] config = ConfigManager([ConfigObjEnv(parsed_vals)]) assert config("debug", parser=bool, raise_error=False) is NO_VALUE parsed_vals = parser.parse_known_args(["--debug=y"])[0] config = ConfigManager([ConfigObjEnv(parsed_vals)]) assert config("debug", parser=bool) is True def test_with_argparse_actions(self): parser = argparse.ArgumentParser() parser.add_argument( "--debug", help="to debug or not to debug", action="store_true" ) parsed_vals = parser.parse_known_args([])[0] config = ConfigManager([ConfigObjEnv(parsed_vals)]) # What happens is that argparse doesn't see an arg, so saves # debug=False. ConfigObjEnv converts that to "False". That gets parsed # as False by the Everett parse_bool function. That's kind of # roundabout but "works" for some/most cases. assert config("debug", parser=bool) is False parsed_vals = parser.parse_known_args(["--debug"])[0] config = ConfigManager([ConfigObjEnv(parsed_vals)]) assert config("debug", parser=bool) is True def test_ConfigDictEnv(): cde = ConfigDictEnv( {"FOO": "bar", "A_FOO": "a_bar", "A_B_FOO": "a_b_bar", "lower_foo": "bar"} ) assert cde.get("foo") == "bar" assert cde.get("foo", namespace=["a"]) == "a_bar" assert cde.get("foo", namespace=["a", "b"]) == "a_b_bar" assert cde.get("FOO", namespace=["a"]) == "a_bar" assert cde.get("foo", namespace=["A"]) == "a_bar" assert cde.get("FOO", namespace=["A"]) == "a_bar" cde = ConfigDictEnv({"foo": "bar"}) assert cde.get("foo") == "bar" assert cde.get("FOO") == "bar" def test_ConfigOSEnv(): os.environ["EVERETT_TEST_FOO"] = "bar" os.environ["EVERETT_TEST_FOO"] = "bar" cose = ConfigOSEnv() assert cose.get("everett_test_foo") == "bar" assert cose.get("EVERETT_test_foo") == "bar" assert cose.get("foo", namespace=["everett", "test"]) == "bar" def test_ConfigEnvFileEnv(datadir): env_filename = os.path.join(datadir, ".env") cefe = ConfigEnvFileEnv(["/does/not/exist/.env", env_filename]) assert cefe.get("not_a", namespace="youre") == "golfer" assert cefe.get("loglevel") == "walter" assert cefe.get("LOGLEVEL") == "walter" assert cefe.get("_typer_standard_traceback") == "1" assert cefe.get("missing") is NO_VALUE assert cefe.data == { "LOGLEVEL": "walter", "DEBUG": "True", "YOURE_NOT_A": "golfer", "DATABASE_URL": "sqlite:///kahlua.db", "_TYPER_STANDARD_TRACEBACK": "1", } cefe = ConfigEnvFileEnv(env_filename) assert cefe.get("not_a", namespace="youre") == "golfer" cefe = ConfigEnvFileEnv("/does/not/exist/.env") assert cefe.get("loglevel") is NO_VALUE @pytest.mark.parametrize( "line, expected", [ ("PLAN9=outerspace", {"PLAN9": "outerspace"}), ('KEY="val"', {"KEY": "val"}), ("KEY='val'", {"KEY": "val"}), # Only strips outer-most quote ("KEY=\"'val'\"", {"KEY": "'val'"}), ("KEY='\"val\"'", {"KEY": '"val"'}), ( "CSP_SCRIPT_SRC='self' www.googletagmanager.com", {"CSP_SCRIPT_SRC": "'self' www.googletagmanager.com"}, ), ( "CSP_SCRIPT_SRC=\"'self' www.googletagmanager.com\"", {"CSP_SCRIPT_SRC": "'self' www.googletagmanager.com"}, ), ], ) def test_parse_env_file_line(line, expected): assert parse_env_file([line]) == expected def test_parse_env_file_errors(): with pytest.raises(ConfigurationError) as exc_info: parse_env_file(["3AMIGOS=infamous"]) assert str(exc_info.value) == "Invalid variable name '3AMIGOS' in env file (line 1)" with pytest.raises(ConfigurationError) as exc_info: parse_env_file(["INVALID-CHAR=value"]) assert str(exc_info.value) == ( "Invalid variable name 'INVALID-CHAR' in env file (line 1)" ) with pytest.raises(ConfigurationError) as exc_info: parse_env_file(["", "MISSING-equals"]) assert str(exc_info.value) == "Env file line missing = operator (line 2)" @pytest.mark.parametrize( "key, ns, expected", [ ("k", None, "K"), ("a_b", None, "A_B"), ("k", "ns", "NS_K"), ("k", ["ns1", "ns2"], "NS1_NS2_K"), ("k", ["ns1", "", "ns2"], "NS1_NS2_K"), ], ) def test_generate_uppercase_key(key, ns, expected): full_key = generate_uppercase_key(key, ns) assert full_key == expected def test_get_key_from_envs(): assert get_key_from_envs({"K": "v"}, "K") == "v" assert get_key_from_envs([{"K": "v"}, {"L": "w"}], "L") == "w" assert get_key_from_envs({"K": "v"}, "Q") is NO_VALUE # first match wins envs = [{"K": "v"}, {"L": "w"}, {"K": "z"}] assert get_key_from_envs(envs, "K") == "v" # works with reversed iterator envs = reversed([{"L": "v"}, {"L": "w"}]) assert get_key_from_envs(envs, "L") == "w" # works with os.environ os.environ["DUDE_ABIDES"] = "yeah, man" assert get_key_from_envs(os.environ, "DUDE_ABIDES") == "yeah, man" def test_config(): config = ConfigManager([]) # Don't raise an error and no default yields NO_VALUE assert config("DOESNOTEXISTNOWAY", raise_error=False) is NO_VALUE # Defaults to raising an error with pytest.raises(ConfigurationMissingError) as exc_info: config("DOESNOTEXISTNOWAY") assert str(exc_info.value) == "DOESNOTEXISTNOWAY requires a value parseable by str" # Raises an error if raise_error is True with pytest.raises(ConfigurationMissingError) as exc_info: config("DOESNOTEXISTNOWAY", raise_error=True) assert str(exc_info.value) == "DOESNOTEXISTNOWAY requires a value parseable by str" # With a default, returns the default assert config("DOESNOTEXISTNOWAY", default="ohreally") == "ohreally" # Test doc with pytest.raises(ConfigurationMissingError) as exc_info: config("DOESNOTEXISTNOWAY", doc="Nothing to see here.") assert str(exc_info.value) == ( "DOESNOTEXISTNOWAY requires a value parseable by str\n" "DOESNOTEXISTNOWAY docs: Nothing to see here." ) def test_raise_configuration_error(): config = ConfigManager.from_dict({"foo_bar": "bat"}) config.doc = "Check https://example.com/configuration for documentation." with pytest.raises(ConfigurationError) as excinfo: config.raise_configuration_error("This is an error") assert str(excinfo.value) == "This is an error\nProject docs: " + config.doc def test_invalidvalueerror(): config = ConfigManager.from_dict({"foo_bar": "bat"}) with pytest.raises(InvalidValueError) as excinfo: config("bar", namespace="foo", parser=bool) assert excinfo.value.namespace == ["foo"] assert excinfo.value.key == "bar" assert excinfo.value.parser == parse_bool def test_configurationmissingerror(): # Verify ConfigurationMissingError has the right values config = ConfigManager([]) # Defaults to raising an error with pytest.raises(ConfigurationMissingError) as exc_info: config("DOESNOTEXISTNOWAY", namespace="foo") assert ( exc_info.value.args[0] == "FOO_DOESNOTEXISTNOWAY requires a value parseable by str" ) assert exc_info.value.namespace == ["foo"] assert exc_info.value.key == "DOESNOTEXISTNOWAY" assert exc_info.value.parser is str def test_config_from_dict(): config = ConfigManager.from_dict({}) assert config("FOO", raise_error=False) is NO_VALUE config = ConfigManager.from_dict({"FOO": "bar"}) assert config("FOO", raise_error=False) == "bar" def test_basic_config(datadir): os.environ["EVERETT_BASIC_CONFIG_TEST"] = "foo" env_filename = os.path.join(datadir, ".env") config = ConfigManager.basic_config(env_filename) # This doesn't exist in either the environment or the env file assert config("FOO", raise_error=False) is NO_VALUE # This exists in the environment assert config("EVERETT_BASIC_CONFIG_TEST") == "foo" # This exists in the env file assert config("LOGLEVEL") == "walter" def test_basic_config_with_docs(datadir): config = ConfigManager.basic_config(doc="foo") assert config.doc == "foo" def test_config_manager_doc(): config = ConfigManager( [ConfigDictEnv({"foo": "bar"})], doc="See http://example.com/configuration" ) # Test ConfigManager doc shows up with pytest.raises(ConfigurationError) as exc_info: config("foo", parser=int) assert str(exc_info.value) == ( "ValueError: invalid literal for int() with base 10: 'bar'\n" "FOO requires a value parseable by int\n" "Project docs: See http://example.com/configuration" ) # Test config doc and ConfigManager doc show up with pytest.raises(ConfigurationError) as exc_info: config("foo", parser=int, doc="Port to listen on.") assert str(exc_info.value) == ( "ValueError: invalid literal for int() with base 10: 'bar'\n" "FOO requires a value parseable by int\n" "FOO docs: Port to listen on.\n" "Project docs: See http://example.com/configuration" ) def test_config_override(): config = ConfigManager([]) # Make sure the key doesn't exist assert config("DOESNOTEXISTNOWAY", raise_error=False) is NO_VALUE # Try one override with config_override(DOESNOTEXISTNOWAY="bar"): assert config("DOESNOTEXISTNOWAY") == "bar" # Try nested overrides--innermost one rules supreme! with config_override(DOESNOTEXISTNOWAY="bar"): with config_override(DOESNOTEXISTNOWAY="bat"): assert config("DOESNOTEXISTNOWAY") == "bat" def test_default_must_be_string(): config = ConfigManager([]) with pytest.raises(ConfigurationError): assert config("DOESNOTEXIST", default=True) def test_default_if_empty(): config = ConfigManager.from_dict({"FOO": ""}) assert config("FOO", default="5", parser=int) == 5 assert config("FOO", default="5", parser=int, default_if_empty=True) == 5 with pytest.raises(InvalidValueError): assert config("FOO", default="5", parser=int, default_if_empty=False) def test_with_namespace(): config = ConfigManager( [ConfigDictEnv({"FOO_BAR": "foobaz", "BAR": "baz", "BAT": "bat"})] ) # Verify the values first assert config("bar", namespace=["foo"]) == "foobaz" assert config("bar") == "baz" assert config("bat") == "bat" # Create the namespaced config config_with_namespace = config.with_namespace("foo") assert config_with_namespace("bar") == "foobaz" # Verify 'bat' is not available because it's not in the namespace with pytest.raises(ConfigurationError): config_with_namespace("bat") def test_get_namespace(): config = ConfigManager.from_dict( {"FOO": "abc", "FOO_BAR": "abc", "FOO_BAR_BAZ": "abc"} ) assert config.get_namespace() == [] ns_foo_config = config.with_namespace("foo") assert ns_foo_config.get_namespace() == ["foo"] ns_foo_bar_config = ns_foo_config.with_namespace("bar") assert ns_foo_bar_config.get_namespace() == ["foo", "bar"] @pytest.mark.parametrize( "key,alternate_keys,expected", [ # key, alternate keys, expected ("FOO", [], "foo_abc"), ("FOO", ["FOO_BAR"], "foo_abc"), ("BAD_KEY", ["FOO_BAR"], "foo_bar_abc"), ("BAD_KEY", ["BAD_KEY1", "BAD_KEY2", "FOO_BAR_BAZ"], "foo_bar_baz_abc"), ], ) def test_alternate_keys(key, alternate_keys, expected): config = ConfigManager.from_dict( {"FOO": "foo_abc", "FOO_BAR": "foo_bar_abc", "FOO_BAR_BAZ": "foo_bar_baz_abc"} ) assert config(key, alternate_keys=alternate_keys) == expected @pytest.mark.parametrize( "key,alternate_keys,expected", [ # key, alternate keys, expected ("BAR", [], "foo_bar_abc"), ("BAD_KEY", ["BAD_KEY1", "BAR_BAZ"], "foo_bar_baz_abc"), ("bad_key", ["bad_key1", "bar_baz"], "foo_bar_baz_abc"), ("bad_key", ["root:common_foo"], "common_foo_abc"), ], ) def test_alternate_keys_with_namespace(key, alternate_keys, expected): config = ConfigManager.from_dict( { "COMMON_FOO": "common_foo_abc", "FOO": "foo_abc", "FOO_BAR": "foo_bar_abc", "FOO_BAR_BAZ": "foo_bar_baz_abc", } ) config = config.with_namespace("FOO") assert config(key, alternate_keys=alternate_keys) == expected def test_raw_value(): config = ConfigManager.from_dict({"FOO_BAR": "1"}) assert config("FOO_BAR", parser=int) == 1 assert config("FOO_BAR", parser=int, raw_value=True) == "1" assert str(config("NOEXIST", parser=int, raise_error=False)) == "NO_VALUE" config = config.with_namespace("FOO") assert config("BAR", parser=int) == 1 assert config("BAR", parser=int, raw_value=True) == "1" def test_with_options(): """Verify .with_options() restricts configuration""" config = ConfigManager( [ConfigDictEnv({"FOO_BAR": "a", "FOO_BAZ": "b", "BAR": "c", "BAZ": "d"})] ) class SomeComponent: class Config: baz = Option(default="", doc="some help here", parser=str) def __init__(self, config): self.config = config.with_options(self) # Create the component with regular config comp = SomeComponent(config) assert comp.config("baz") == "d" with pytest.raises(ConfigurationError): # This is not a valid option for this component comp.config("bar") # Create the component with config in the "foo" namespace comp2 = SomeComponent(config.with_namespace("foo")) assert comp2.config("baz") == "b" with pytest.raises(ConfigurationError): # This is not a valid option for this component comp2.config("bar") def test_nested_options(): """Verify nested BoundOptions works.""" class Foo: class Config: option1 = Option(default="opt1default", parser=str) class Bar: class Config: option2 = Option(default="opt2default", parser=str) config = ConfigManager.basic_config() config = config.with_options(Foo) config = config.with_options(Bar) assert config("option2") == "opt2default" with pytest.raises(ConfigurationError): config("option1") def test_namespace_and_options(): """Verify namespace and then options works""" class Foo: class Config: option = Option(default="opt1default", parser=str) config = ConfigManager.from_dict({"NS_OPTION": "val"}) config = config.with_namespace("ns").with_options(Foo) assert config("option") == "val" def test_options_and_namespace(): """Verify options and then namespace works""" # NOTE(willkg): This doesn't make sense to me, but there's no technical # reason it shouldn't work. class App: class Config: http_port = Option(default="80") db_port = Option(default="3306") config = ConfigManager.from_dict({}) config = config.with_options(App) http_config = config.with_namespace("http") db_config = config.with_namespace("db") assert http_config("port") == "80" assert db_config("port") == "3306" def test_default_comes_from_options(): """Verify that the default is picked up from options""" config = ConfigManager([]) class SomeComponent: class Config: foo = Option(default="abc") def __init__(self, config): self.config = config.with_options(self) comp = SomeComponent(config) assert comp.config("foo") == "abc" def test_parser_comes_from_options(): """Verify the parser is picked up from options""" config = ConfigManager([ConfigDictEnv({"FOO": "1"})]) class SomeComponent: class Config: foo = Option(parser=int) def __init__(self, config): self.config = config.with_options(self) comp = SomeComponent(config) assert comp.config("foo") == 1 def test_component_get_namespace(): config = ConfigManager.from_dict( {"FOO": "abc", "FOO_BAR": "abc", "FOO_BAR_BAZ": "abc"} ) assert config.get_namespace() == [] class SomeComponent: class Config: foo = Option(parser=int) def __init__(self, config): self.config = config.with_options(self) def my_namespace_is(self): return self.config.get_namespace() comp = SomeComponent(config) assert comp.my_namespace_is() == [] comp = SomeComponent(config.with_namespace("foo")) assert comp.my_namespace_is() == ["foo"] def test_component_alternate_keys(): config = ConfigManager.from_dict( {"COMMON": "common_abc", "FOO": "abc", "FOO_BAR": "abc", "FOO_BAR_BAZ": "abc"} ) class SomeComponent: class Config: bad_key = Option(alternate_keys=["root:common"]) def __init__(self, config): self.config = config.with_options(self) comp = SomeComponent(config) # The key is invalid, so it tries the alternate keys assert comp.config("bad_key") == "common_abc" def test_component_doc(): config = ConfigManager.from_dict({"FOO_BAR": "bat"}) class SomeComponent: class Config: foo_bar = Option(parser=int, doc="omg!") def __init__(self, config): self.config = config.with_options(self) comp = SomeComponent(config) try: # This throws an exception becase "bat" is not an int comp.config("foo_bar") except Exception as exc: # We're going to lazily assert that omg! is in exc msg because if it # is, it came from the option and that's what we want to know. assert "omg!" in str(exc) def test_component_raw_value(): config = ConfigManager.from_dict({"FOO_BAR": "1"}) class SomeComponent: class Config: foo_bar = Option(parser=int) def __init__(self, config): self.config = config.with_options(self) comp = SomeComponent(config) assert comp.config("foo_bar") == 1 assert comp.config("foo_bar", raw_value=True) == "1" class SomeComponent: class Config: bar = Option(parser=int) def __init__(self, config): self.config = config.with_options(self) comp = SomeComponent(config.with_namespace("foo")) assert comp.config("bar") == 1 assert comp.config("bar", raw_value=True) == "1" class TestGetRuntimeConfig: def test_bound_config(self): config = ConfigManager.from_dict({"foo": 12345}) class ComponentA: class Config: foo = Option(parser=int) bar = Option(parser=int, default="1") def __init__(self, config): self.config = config.with_options(self) comp = ComponentA(config) assert list(get_runtime_config(config, comp)) == [ ([], "foo", "12345", Option(parser=int)), ([], "bar", "1", Option(parser=int, default="1")), ] def test_tree_with_specified_namespace(self): config = ConfigManager.from_dict({}) class ComponentB: class Config: foo = Option(parser=int, default="2") bar = Option(parser=int, default="1") def __init__(self, config): self.config = config.with_options(self) class ComponentA: class Config: baz = Option(default="abc") def __init__(self, config): self.config = config.with_options(self) self.biff = ComponentB(config.with_namespace("biff")) comp = ComponentA(config) assert list(get_runtime_config(config, comp)) == [ ([], "baz", "abc", Option(default="abc")), (["biff"], "foo", "2", Option(parser=int, default="2")), (["biff"], "bar", "1", Option(parser=int, default="1")), ] def test_tree_inferred_namespace(self): """Test get_runtime_config can pull namespace from config.""" class ComponentB: class Config: foo = Option(parser=int, default="2") bar = Option(parser=int, default="1") def __init__(self, config): self.config = config.with_options(self) class ComponentA: class Config: baz = Option(default="abc") def __init__(self, config): self.config = config.with_options(self) self.boff = ComponentB(config.with_namespace("boff")) config = ConfigManager.from_dict({}) comp = ComponentA(config) assert list(get_runtime_config(config, comp)) == [ ([], "baz", "abc", Option(default="abc")), (["boff"], "foo", "2", Option(parser=int, default="2")), (["boff"], "bar", "1", Option(parser=int, default="1")), ] def test_slots(self): """Test get_runtime_config works with classes using slots.""" config = ConfigManager.from_dict({}) class Base: __slots__ = ("_slotattr",) class ComponentA(Base): class Config: key = Option(default="abc") def __init__(self, config_manager): self.config = config_manager.with_options(self) comp = ComponentA(config) assert list(get_runtime_config(config, comp)) == [ ([], "key", "abc", Option(default="abc")) ] ================================================ FILE: tests/test_sphinxext.py ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Test sphinxext directives.""" from textwrap import dedent import pytest from sphinx.cmd.build import main as sphinx_main def run_sphinx(docsdir, text, builder="text"): # set up conf.py with open(str(docsdir / "conf.py"), "w") as fp: fp.write('master_doc = "index"\n') fp.write('extensions = ["everett.sphinxext"]\n') # set up index.rst file text = "BEGINBEGIN\n\n%s\n\nENDEND" % text with open(str(docsdir / "index.rst"), "w") as fp: fp.write(text) args = ["-b", builder, "-v", "-E", str(docsdir), str(docsdir / "_build" / builder)] print(args) if sphinx_main(args): raise RuntimeError("Sphinx build failed") extension = "txt" if builder == "text" else "html" with open( str(docsdir / "_build" / builder / f"index.{extension}"), encoding="utf8" ) as fp: data = fp.read() # index.text has a bunch of stuff in it. BEGINBEGIN and ENDEND are markers, # so we just return the bits in between. data = data[data.find("BEGINBEGIN") + 10 : data.find("ENDEND")] # Strip the whitespace, but add a \n to make tests easier to read. data = data.strip() + "\n" return data def test_infrastructure(tmpdir): # Verify parsing is working at all. This seems like a no-op, but really # it's going through all the Sphinx stuff to generate the text that it # started off with. assert run_sphinx(tmpdir, "*foo*") == dedent( """\ *foo* """ ) def test_everett_component(tmpdir, capsys): # Test .. everett:component:: with an option and verify Sphinx isn't # spitting out warnings rst = dedent( """\ .. everett:component:: mymodule.ComponentBasic .. everett:option:: opt1 :parser: str :default: "foo" First option. """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component mymodule.ComponentBasic opt1 Parser: *str* Default: "foo" Required: No First option. """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err class Test_autocomponentconfig: def test_basic(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentBasic """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentBasic user Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_no_option_data(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentNoOptions """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentNoOptions No configuration options. """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_hide_name(self, tmpdir, capsys): # Test hide-name rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentBasic :hide-name: """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ Configuration user Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_namespace(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentBasic :namespace: foo """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentBasic foo_user Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_case_bad_value(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentBasic :case: foo """ ) # Because "foo" isn't valid, nothing ends up in the file assert run_sphinx(tmpdir, rst) == "\n" captured = capsys.readouterr() assert "WARNING" not in captured.out assert 'argument must be "upper", "lower" or None.' in captured.err def test_case_lower(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentBasic :case: lower """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentBasic user Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_case_upper(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentBasic :case: upper """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentBasic USER Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_show_docstring_class_has_no_docstring(self, tmpdir, capsys): # Test docstring-related things rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentBasic :show-docstring: """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentBasic Basic component. Multiple lines. user Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_show_docstring(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentWithDocstring :show-docstring: """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentWithDocstring This component is the best. The best! user Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_show_docstring_other_attribute(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentDocstringOtherAttribute :show-docstring: __everett_help__ """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentDocstringOtherAttribute User-focused help user Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_show_docstring_subclass(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentSubclass :show-docstring: """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentSubclass A different docstring. user Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_option_default(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentOptionDefault """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentOptionDefault user Parser: *str* Default: "ou812" Required: No """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_option_doc(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentOptionDoc """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentOptionDoc user Parser: *str* Required: Yes ou812 """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_option_doc_multiline(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentOptionDocMultiline """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentOptionDocMultiline user Parser: *str* Required: Yes ou812 password Parser: *str* Required: Yes First "paragraph". Second paragraph. """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_option_doc_default(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentOptionDocDefault """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentOptionDocDefault user Parser: *str* Default: "ou812" Required: No This is some docs. """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_option_parser(self, tmpdir, capsys): rst = dedent( """\ .. autocomponentconfig:: basic_component_config.ComponentOptionParser """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_component_config.ComponentOptionParser user_builtin Parser: *int* Required: Yes user_parse_class Parser: *everett.manager.parse_class* Required: Yes user_listof Parser: ** Required: Yes user_class_method Parser: *basic_component_config.Foo.parse_foo_class* Required: Yes user_instance_method Parser: *basic_component_config.Foo.parse_foo_instance* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err class Test_automoduleconfig: def test_basic(self, tmpdir, capsys): rst = dedent( """\ .. automoduleconfig:: basic_module_config._config """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component basic_module_config._config debug Parser: *bool* Default: "False" Required: No Debug mode. logging_level Parser: *parse_logging_level* Required: Yes Level. password Parser: *str* Required: Yes Password field. Must be provided. fun Parser: *int if 0 else float* Required: Yes Woah. long_description Parser: *str* Default: "" Required: No This configuration item has a really long description that spans several lines so we can test runtime string concatenation. Multiple lines should work, too. cache_location Parser: *str* Default: "127.0.0.1:11211" Required: No The location """ ) captured = capsys.readouterr() print(captured.out) assert "WARNING" not in captured.out print(captured.err) assert "WARNING" not in captured.err def test_hide_name(self, tmpdir, capsys): rst = dedent( """\ .. automoduleconfig:: simple_module_config._config :hide-name: """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ Configuration host Parser: *str* Default: "localhost" Required: No The host. """ ) def test_namespace(self, tmpdir, capsys): rst = dedent( """\ .. automoduleconfig:: simple_module_config._config :namespace: app """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component simple_module_config._config app_host Parser: *str* Default: "localhost" Required: No The host. """ ) def test_case_upper(self, tmpdir, capsys): rst = dedent( """\ .. automoduleconfig:: simple_module_config._config :case: upper """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component simple_module_config._config HOST Parser: *str* Default: "localhost" Required: No The host. """ ) def test_case_lower(self, tmpdir, capsys): rst = dedent( """\ .. automoduleconfig:: simple_module_config._config :case: lower """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component simple_module_config._config host Parser: *str* Default: "localhost" Required: No The host. """ ) def test_show_table(self, tmpdir, capsys): rst = dedent( """\ .. automoduleconfig:: simple_module_config._config :show-table: """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component simple_module_config._config Configuration summary: +--------------------------------------------------------------+----------+-------------+ | Setting | Parser | Required? | |==============================================================|==========|=============| | "host" | *str* | | +--------------------------------------------------------------+----------+-------------+ Configuration options: host Parser: *str* Default: "localhost" Required: No The host. """ ) def test_show_docstring(self, tmpdir, capsys): rst = dedent( """\ .. automoduleconfig:: simple_module_config._config :show-docstring: """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component simple_module_config._config Simple module config. host Parser: *str* Default: "localhost" Required: No The host. """ ) def test_show_docstring_by_attribute(self, tmpdir, capsys): rst = dedent( """\ .. automoduleconfig:: simple_module_config._config :show-docstring: HELP """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component simple_module_config._config Help attribute value. host Parser: *str* Default: "localhost" Required: No The host. """ ) def test_import_error(self, tmpdir, capsys): rst = dedent( """\ .. automoduleconfig:: badpath """ ) with pytest.raises(RuntimeError): run_sphinx(tmpdir, rst) captured = capsys.readouterr() assert ( "ValueError: 'badpath' does not point to a valid thing" in captured.err ) class Test_everett_option: def test_basic(self, tmpdir, capsys): rst = dedent( """\ .. everett:option:: debug """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ debug Parser: *str* Required: Yes """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err def test_thorough(self, tmpdir, capsys): rst = dedent( """\ .. everett:option:: debug :parser: bool :default: "false" Set to "true" for debug mode """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ debug Parser: *bool* Default: "false" Required: No Set to "true" for debug mode """ ) def test_no_default_means_required(self, tmpdir, capsys): rst = dedent( """\ .. everett:option:: debug :parser: bool Set to "true" for debug mode """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ debug Parser: *bool* Required: Yes Set to "true" for debug mode """ ) class Test_everett_component: def test_basic(self, tmpdir, capsys): rst = dedent( """\ .. everett:component:: MyClass Some details about my class. .. everett:option:: debug :parser: bool Set to "true" for debug mode """ ) assert run_sphinx(tmpdir, rst) == dedent( """\ component MyClass Some details about my class. debug Parser: *bool* Required: Yes Set to "true" for debug mode """ ) captured = capsys.readouterr() assert "WARNING" not in captured.out assert "WARNING" not in captured.err