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