Metadata-Version: 2.4
Name: kwconf
Version: 0.12.0
Summary: Typed, dependency-free configuration objects with CLI, environment, and file parsing (a scriptconfig successor).
Author-email: "Kitware Inc." <kitware@kitware.com>, Jon Crall <jon.crall@kitware.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/Erotemic/kwconf
Project-URL: Repository, https://github.com/Erotemic/kwconf
Project-URL: Documentation, https://kwconf.readthedocs.io
Project-URL: Issues, https://github.com/Erotemic/kwconf/issues
Keywords: configuration,config,cli,argparse,scriptconfig,yaml
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Utilities
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE
Provides-Extra: all
Requires-Dist: pytest>=8.1.1; (python_version < "4.0" and python_version >= "3.13") and extra == "all"
Requires-Dist: pytest>=8.1.1; (python_version < "3.13" and python_version >= "3.12") and extra == "all"
Requires-Dist: pytest>=8.1.1; (python_version < "3.12" and python_version >= "3.11") and extra == "all"
Requires-Dist: pytest>=6.2.5; (python_version < "3.11" and python_version >= "3.10") and extra == "all"
Requires-Dist: pytest>=6.2.5; (python_version < "3.10" and python_version >= "3.8") and extra == "all"
Requires-Dist: coverage>=6.1.1; python_version >= "3.10" and extra == "all"
Requires-Dist: coverage>=5.3.1; (python_version < "3.10" and python_version >= "3.9") and extra == "all"
Requires-Dist: coverage>=6.1.1; (python_version < "3.9" and python_version >= "3.8") and extra == "all"
Requires-Dist: pytest-cov; extra == "all"
Requires-Dist: xdoctest>=1.1.5; extra == "all"
Requires-Dist: ubelt>=1.3.6; extra == "all"
Requires-Dist: PyYAML>=6.0.1; (python_version < "4.0" and python_version >= "3.12") and extra == "all"
Requires-Dist: PyYAML>=6.0; (python_version < "3.12" and python_version >= "3.11") and extra == "all"
Requires-Dist: PyYAML>=6.0; (python_version < "3.11" and python_version >= "3.10") and extra == "all"
Requires-Dist: numpy>=2.1.0; (python_version < "4.0" and python_version >= "3.13") and extra == "all"
Requires-Dist: numpy>=1.26.0; (python_version < "3.13" and python_version >= "3.12") and extra == "all"
Requires-Dist: numpy>=1.23.2; (python_version < "3.12" and python_version >= "3.11") and extra == "all"
Requires-Dist: numpy>=1.21.6; (python_version < "3.11" and python_version >= "3.10") and extra == "all"
Requires-Dist: numpy>=1.19.3; (python_version < "3.10" and python_version >= "3.9") and extra == "all"
Requires-Dist: numpy>=1.19.2; (python_version < "3.9" and python_version >= "3.8") and extra == "all"
Requires-Dist: numpy>=1.14.5; (python_version < "3.8" and python_version >= "3.7") and extra == "all"
Requires-Dist: numpy>=1.12.0; (python_version < "3.7" and python_version >= "3.6") and extra == "all"
Requires-Dist: numpy>=1.11.1; (python_version < "3.6" and python_version >= "3.5") and extra == "all"
Requires-Dist: numpy>=1.11.1; (python_version < "3.5" and python_version >= "3.4") and extra == "all"
Requires-Dist: numpy>=1.11.1; (python_version < "3.4" and python_version >= "2.7") and extra == "all"
Requires-Dist: rich_argparse>=1.1.0; python_version >= "3.7" and extra == "all"
Requires-Dist: argcomplete>=3.6.3; python_version >= "3.13" and extra == "all"
Requires-Dist: argcomplete>=3.5.2; (python_version < "3.13" and python_version >= "3.12") and extra == "all"
Requires-Dist: argcomplete>=3.3.0; (python_version < "3.12" and python_version >= "3.11") and extra == "all"
Requires-Dist: argcomplete>=3.0.5; (python_version < "3.11" and python_version >= "3.8") and extra == "all"
Requires-Dist: sphinx>=5.0.1; extra == "all"
Requires-Dist: sphinx-autobuild>=2021.3.14; extra == "all"
Requires-Dist: sphinx_rtd_theme>=1.0.0; extra == "all"
Requires-Dist: sphinxcontrib-napoleon>=0.7; extra == "all"
Requires-Dist: sphinx-autoapi>=1.8.4; extra == "all"
Requires-Dist: Pygments>=2.9.0; extra == "all"
Requires-Dist: myst_parser>=0.18.0; extra == "all"
Requires-Dist: sphinx-reredirects>=0.0.1; extra == "all"
Requires-Dist: flake8>=5.0.0; extra == "all"
Requires-Dist: ruff>=0.6.0; extra == "all"
Requires-Dist: PyYAML>=6.0.1; (python_version < "4.0" and python_version >= "3.12") and extra == "all"
Requires-Dist: PyYAML>=6.0; (python_version < "3.12" and python_version >= "3.11") and extra == "all"
Requires-Dist: PyYAML>=6.0; (python_version < "3.11" and python_version >= "3.10") and extra == "all"
Requires-Dist: PyYAML>=5.4.1; (python_version < "3.10" and python_version >= "3.9") and extra == "all"
Requires-Dist: PyYAML>=5.4.1; (python_version < "3.9" and python_version >= "3.8") and extra == "all"
Requires-Dist: ubelt>=1.3.6; extra == "all"
Provides-Extra: docs
Requires-Dist: sphinx>=5.0.1; extra == "docs"
Requires-Dist: sphinx-autobuild>=2021.3.14; extra == "docs"
Requires-Dist: sphinx_rtd_theme>=1.0.0; extra == "docs"
Requires-Dist: sphinxcontrib-napoleon>=0.7; extra == "docs"
Requires-Dist: sphinx-autoapi>=1.8.4; extra == "docs"
Requires-Dist: Pygments>=2.9.0; extra == "docs"
Requires-Dist: myst_parser>=0.18.0; extra == "docs"
Requires-Dist: sphinx-reredirects>=0.0.1; extra == "docs"
Provides-Extra: linting
Requires-Dist: flake8>=5.0.0; extra == "linting"
Requires-Dist: ruff>=0.6.0; extra == "linting"
Provides-Extra: optional
Requires-Dist: numpy>=2.1.0; (python_version < "4.0" and python_version >= "3.13") and extra == "optional"
Requires-Dist: numpy>=1.26.0; (python_version < "3.13" and python_version >= "3.12") and extra == "optional"
Requires-Dist: numpy>=1.23.2; (python_version < "3.12" and python_version >= "3.11") and extra == "optional"
Requires-Dist: numpy>=1.21.6; (python_version < "3.11" and python_version >= "3.10") and extra == "optional"
Requires-Dist: numpy>=1.19.3; (python_version < "3.10" and python_version >= "3.9") and extra == "optional"
Requires-Dist: numpy>=1.19.2; (python_version < "3.9" and python_version >= "3.8") and extra == "optional"
Requires-Dist: numpy>=1.14.5; (python_version < "3.8" and python_version >= "3.7") and extra == "optional"
Requires-Dist: numpy>=1.12.0; (python_version < "3.7" and python_version >= "3.6") and extra == "optional"
Requires-Dist: numpy>=1.11.1; (python_version < "3.6" and python_version >= "3.5") and extra == "optional"
Requires-Dist: numpy>=1.11.1; (python_version < "3.5" and python_version >= "3.4") and extra == "optional"
Requires-Dist: numpy>=1.11.1; (python_version < "3.4" and python_version >= "2.7") and extra == "optional"
Requires-Dist: rich_argparse>=1.1.0; python_version >= "3.7" and extra == "optional"
Requires-Dist: argcomplete>=3.6.3; python_version >= "3.13" and extra == "optional"
Requires-Dist: argcomplete>=3.5.2; (python_version < "3.13" and python_version >= "3.12") and extra == "optional"
Requires-Dist: argcomplete>=3.3.0; (python_version < "3.12" and python_version >= "3.11") and extra == "optional"
Requires-Dist: argcomplete>=3.0.5; (python_version < "3.11" and python_version >= "3.8") and extra == "optional"
Provides-Extra: tests
Requires-Dist: pytest>=8.1.1; (python_version < "4.0" and python_version >= "3.13") and extra == "tests"
Requires-Dist: pytest>=8.1.1; (python_version < "3.13" and python_version >= "3.12") and extra == "tests"
Requires-Dist: pytest>=8.1.1; (python_version < "3.12" and python_version >= "3.11") and extra == "tests"
Requires-Dist: pytest>=6.2.5; (python_version < "3.11" and python_version >= "3.10") and extra == "tests"
Requires-Dist: pytest>=6.2.5; (python_version < "3.10" and python_version >= "3.8") and extra == "tests"
Requires-Dist: coverage>=6.1.1; python_version >= "3.10" and extra == "tests"
Requires-Dist: coverage>=5.3.1; (python_version < "3.10" and python_version >= "3.9") and extra == "tests"
Requires-Dist: coverage>=6.1.1; (python_version < "3.9" and python_version >= "3.8") and extra == "tests"
Requires-Dist: pytest-cov; extra == "tests"
Requires-Dist: xdoctest>=1.1.5; extra == "tests"
Requires-Dist: ubelt>=1.3.6; extra == "tests"
Requires-Dist: PyYAML>=6.0.1; (python_version < "4.0" and python_version >= "3.12") and extra == "tests"
Requires-Dist: PyYAML>=6.0; (python_version < "3.12" and python_version >= "3.11") and extra == "tests"
Requires-Dist: PyYAML>=6.0; (python_version < "3.11" and python_version >= "3.10") and extra == "tests"
Provides-Extra: ubelt
Requires-Dist: ubelt>=1.3.6; extra == "ubelt"
Provides-Extra: yaml
Requires-Dist: PyYAML>=6.0.1; (python_version < "4.0" and python_version >= "3.12") and extra == "yaml"
Requires-Dist: PyYAML>=6.0; (python_version < "3.12" and python_version >= "3.11") and extra == "yaml"
Requires-Dist: PyYAML>=6.0; (python_version < "3.11" and python_version >= "3.10") and extra == "yaml"
Requires-Dist: PyYAML>=5.4.1; (python_version < "3.10" and python_version >= "3.9") and extra == "yaml"
Requires-Dist: PyYAML>=5.4.1; (python_version < "3.9" and python_version >= "3.8") and extra == "yaml"
Dynamic: license-file

kwconf - Keyword Configuration
==============================

|Pypi| |PypiDownloads| |ReadTheDocs| |GithubActions| |Codecov| |GitlabCIPipeline| |GitlabCICoverage|

Kitware's keyword configuration module: ``kwconf`` defines small configuration
objects that work from Python kwargs, command line arguments, environment
variables, and JSON/YAML files. It is the successor to `scriptconfig
<https://pypi.org/project/scriptconfig>`_, with the same small-script
ergonomics and a clearer parser model.

+-----------------+-----------------------------------------+
| Read the Docs   | http://kwconf.readthedocs.io/en/latest/ |
+-----------------+-----------------------------------------+
| Github          | https://github.com/Erotemic/kwconf      |
+-----------------+-----------------------------------------+
| Pypi            | https://pypi.org/project/kwconf         |
+-----------------+-----------------------------------------+

Features
--------

* Define config once, then read it from kwargs, argv, env, or files.
* Use the object like a dataclass, dict, or argparse namespace.
* Start with plain defaults. Add ``Value`` for help text, aliases, choices,
  flags, positions, ``nargs``, default factories, or a custom parser.
* Coerce only string-only sources: ``sys.argv`` tokens and ``os.environ``
  values. Python values are used as Python values.
* Use the default parsers: ``auto`` for scalars, ``csv`` for comma lists, and
  ``yaml`` for YAML-shaped values.
* Build argparse-backed CLIs, modal subcommands, nested config trees, dotted
  overrides, and YAML/JSON load/dump.
* Ship with ``py.typed`` and zero required runtime dependencies.

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

.. code-block:: bash

    pip install kwconf

    # optional extras
    pip install kwconf[yaml]    # YAML config load/dump and parser='yaml'
    pip install kwconf[ubelt]   # rich repr, Config.__json__, port_to_argparse

Quickstart
----------

Start with plain class attributes. Type annotations are optional.

.. code-block:: python

    import kwconf


    class DemoConfig(kwconf.Config):
        count = 1
        mode = kwconf.Value('fast', choices=['fast', 'safe'])
        tags = kwconf.Value(default_factory=list, nargs='+')


    cfg = DemoConfig.cli(argv=['--count=3', '--mode=safe', '--tags', 'a', 'b'])
    assert cfg.count == 3
    assert cfg['mode'] == 'safe'
    assert cfg.tags == ['a', 'b']

The same class works from Python, files, env, or argv:

.. code-block:: python

    cfg = DemoConfig(count=2)
    cfg = DemoConfig().load({'count': 2})
    cfg = DemoConfig.cli(data={'count': 2}, argv=False)
    cfg = DemoConfig.cli(argv='--count=2 --mode=safe')
    cfg = DemoConfig.from_env(prefix='DEMO_')
    cfg = DemoConfig.from_yaml('demo.yaml')

Parser basics
-------------

A parser tells a field how to read a CLI/env string.

.. code-block:: python

    import kwconf


    class ParserConfig(kwconf.Config):
        scalar = kwconf.Value(None)                         # parser='auto'
        nums = kwconf.Value(default_factory=list, parser='csv')
        payload = kwconf.Value(None, parser='yaml')


    cfg = ParserConfig.cli(argv=[
        '--scalar=3',
        '--nums=1,2,3',
        '--payload={enabled: true, size: 4}',
    ])
    assert cfg.scalar == 3
    assert cfg.nums == [1, 2, 3]
    assert cfg.payload == {'enabled': True, 'size': 4}

``auto`` reads scalar strings such as ``3``, ``true``, and ``null``.
``csv`` splits commas and applies ``auto`` to each part. ``yaml`` uses
``yaml.safe_load`` for lists, dicts, and scalars; install ``kwconf[yaml]`` for
that parser. See the `coercion manual
<http://kwconf.readthedocs.io/en/latest/manual/coercion_and_cli.html>`_ for
the detailed parser contract.

Flags and bare options
----------------------

Kwconf deliberately lets flags be written both conveniently and explicitly.
For example, ``--flag`` means the flag's bare value while ``--flag=false`` or
``--flag false`` records an explicit false value on the command line. This is a
core kwconf feature: explicit configurations do not need to delete false-valued
keys.

``bare=`` generalizes the same idea to non-boolean values:

.. code-block:: python

    class ArchiveConfig(kwconf.Config):
        patch = kwconf.Value(None, bare='auto', short_alias=['p'])
        verbose = kwconf.Value(0, isflag='counter', short_alias=['v'])


    assert ArchiveConfig.cli(argv=['--patch']).patch == 'auto'
    assert ArchiveConfig.cli(argv=['--patch=base.tar']).patch == 'base.tar'
    assert ArchiveConfig.cli(argv=['-pv']).patch == 'auto'
    assert ArchiveConfig.cli(argv=['-pv']).verbose == 1

Bare-capable short aliases are clusterable. They intentionally do not accept
undelimited attached values: use ``-p=file`` or ``-p file``, not ``-pfile``.
Ordinary required-value aliases continue to accept argparse's ``-kVALUE``
syntax. Use ``--`` when a token following a bare option must be positional,
for example ``prog --flag -- input.txt``.

The lexical conveniences can be disabled independently with
``__fuzzy_hyphens__ = False`` and ``__short_alias_clusters__ = False``. See the
`coercion and CLI contract
<http://kwconf.readthedocs.io/en/latest/manual/coercion_and_cli.html>`_ for the
full grammar.

Growing a script
----------------

``kwconf`` is designed for scripts that start as a dictionary and grow into a
CLI with minimal churn.

.. code-block:: python

    import kwconf


    class MyConfig(kwconf.Config):
        simple_option1 = 1
        simple_option2 = 2


    def main(argv=None, **kwargs):
        config = MyConfig.cli(argv=argv, data=kwargs)
        return run_algorithm(config)


    def run_algorithm(config):
        # Existing dict-style code can keep using config['simple_option1'].
        ...

Add metadata where the CLI needs it:

.. code-block:: python

    class MyConfig(kwconf.Config):
        simple_option1 = kwconf.Value(1, help='first simple option')
        simple_option2 = kwconf.Value(2, help='second simple option')

Typed path
----------

Annotations improve static checks, editor help, parser selection, and runtime
validation.

.. code-block:: python

    class TrainConfig(kwconf.Config):
        lr: float = 1e-3
        mode: str = kwconf.Value('fast', choices=['fast', 'safe'])
        tags: list[str] = kwconf.Value(default_factory=list, nargs='+')


    cfg = TrainConfig.cli(argv=['--lr=0.01', '--tags', 'cat', 'dog'])
    assert cfg.lr == 0.01
    assert cfg.tags == ['cat', 'dog']

Runnable examples
-----------------

The checked-in examples live in ``examples/``. Run commands from the repo root:

.. code-block:: bash

    python examples/01_minimal_config.py --help
    python examples/01_minimal_config.py --width=128 --height=96 --method=lanczos --dst=thumb.png --tags demo small --dry-run
    python examples/03_config_files.py --config examples/data/report.yaml --limit=3 --format=json
    python examples/run_all.py

Use ``examples/README.md`` as the map. Each example focuses on one surface:
basic configs, CLI flags, files, nested configs, modals, large app structure,
and migration helpers.

Scriptconfig migration
----------------------

Use the migration guide when porting existing code or prompting an LLM that
already knows scriptconfig.

* ``import scriptconfig as scfg`` -> ``import kwconf``.
* ``scfg.Config`` / ``scfg.DataConfig`` -> ``kwconf.Config``.
* ``type=`` -> ``parser=`` for new code.
* ``cmdline=`` -> ``argv=``. Recent scriptconfig already supports ``argv``;
  older examples often emphasize ``cmdline``.
* ``--config`` / ``--dump`` / ``--dumps`` are opt-in via
  ``special_options=True`` or ``__special_options__ = True``.
* Comma-separated CLI strings stay strings. Use ``nargs='+'``,
  ``parser='csv'``, or ``parser='yaml'`` for structured text input.

See the `migration guide
<http://kwconf.readthedocs.io/en/latest/manual/migration_from_scriptconfig.html>`_
for the checklist, footguns, and exact replacements.

Next steps
----------

Read the `documentation <http://kwconf.readthedocs.io/en/latest/>`_ for the
core contract, parser model, nested configs, modal CLIs, and migration notes.
The ``examples/`` directory contains runnable scripts for the main patterns.


Related Work
------------

``kwconf`` sits at the intersection of argument parsing, settings management,
and typed data models. Like its predecessor `scriptconfig
<https://pypi.org/project/scriptconfig/>`_, it builds on the standard library
`argparse <https://docs.python.org/3/library/argparse.html>`_, but adds flags
that double as key/value pairs, defaults drawn from kwargs / env / files, and
the ability to call a CLI as a plain Python function.

Closely related / complementary
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* `jsonargparse <https://jsonargparse.readthedocs.io/>`_ -- argparse plus config
  files and class / dataclass / pydantic signatures (and the engine behind
  Lightning's CLI). Its nested-configuration support is complementary to
  ``kwconf`` and a natural integration target.
* `configargparse <https://pypi.org/project/ConfigArgParse/>`_ -- augments
  argparse with config-file and env-var defaults, but is more limited and does
  not handle the "call the CLI as a Python function" use case well.

Typed classes to CLIs
~~~~~~~~~~~~~~~~~~~~~~~

Like ``kwconf``, these derive a parser from a class of typed fields (most via
:pep:`681` ``dataclass_transform``):

* `pydantic <https://pypi.org/project/pydantic/>`_ /
  `pydantic-settings <https://pypi.org/project/pydantic-settings/>`_ --
  validated data models, layered settings sources, and model-derived CLIs; see
  the comparison below.
* `pydantic-cli <https://pypi.org/project/pydantic-cli/>`_ -- an argparse CLI
  built from a pydantic model.
* `simple-parsing <https://pypi.org/project/simple-parsing/>`_ -- adds
  dataclasses to ``argparse``.
* `tyro <https://github.com/brentyi/tyro>`_ -- typed CLIs from dataclasses,
  functions, and unions.
* `typer <https://typer.tiangolo.com/>`_ -- ``click``-based CLIs from type hints.

Pydantic and pydantic-settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

`Pydantic <https://docs.pydantic.dev/latest/concepts/models/>`_ ``BaseModel``
classes use type annotations for runtime validation and coercion, serialization,
and JSON Schema generation. `pydantic-settings
<https://docs.pydantic.dev/latest/concepts/pydantic_settings/>`_ adds settings
sources including environment variables, dotenv files, secrets, and command-line
arguments.

``pydantic-settings`` can construct CLIs from ``BaseSettings`` and ordinary
Pydantic ``BaseModel`` classes. Its command-line support includes nested models,
lists and dictionaries, literals and enums, aliases, positional arguments,
subcommands, mutually exclusive groups, boolean flags, unknown-argument handling,
kebab-case names, shortcuts, argument serialization, and integration with an
existing argparse parser.

The main API differences are:

* ``kwconf.Config`` is a mutable mapping with attribute access. Pydantic
  ``BaseModel`` is a model object and exports mappings with ``model_dump()``.
* ``kwconf`` permits fields declared as ordinary class defaults and adds
  annotations or ``Value`` metadata as needed. Pydantic model fields are
  annotation-based.
* ``kwconf.Value(parser=...)`` controls parsing of text inputs. Pydantic applies
  field and model validation at the model boundary.
* ``kwconf`` includes mapping, environment, config-file, and argv ingestion,
  together with opt-in ``--config``, ``--dump``, and ``--dumps`` options.
  ``pydantic-settings`` provides configurable settings sources and source
  precedence.
* ``kwconf`` has no required runtime dependencies. Pydantic and
  ``pydantic-settings`` are separate runtime packages.

Migrating a kwconf Config to Pydantic
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``Config.port_to_pydantic()`` generates Pydantic 2 ``BaseModel`` source::

    class TrainConfig(kwconf.Config):
        workers: int = kwconf.Value(
            4,
            alias=['num_workers'],
            help='Number of workers',
            tags=['perf_param'],
        )

    print(TrainConfig().port_to_pydantic())

The generated source translates field annotations, defaults, importable
``default_factory`` callables, help text, long aliases, JSON-compatible tags,
and simple nested ``SubConfig`` models. Long aliases use Pydantic validation
aliases. Tags are stored in ``json_schema_extra['kwconf_tags']``.

The generator emits ``REVIEW(kwconf-port)`` comments for kwconf metadata without
a direct field-level translation: custom parsers, positional layout, counters,
``nargs``, short aliases, argument groups, mutex groups, choices not expressed by
the annotation, dynamic ``SubConfig`` selectors, and class-level CLI policies.
CLI and settings-source migration to ``pydantic-settings`` is not generated.

Source generation does not import Pydantic and does not add a Pydantic runtime
dependency to ``kwconf``.


CLI frameworks
~~~~~~~~~~~~~~~

* `click <https://pypi.org/project/click/>`_ -- composable decorator-based CLIs.
* `fire <https://pypi.org/project/fire/>`_ -- generates a CLI from arbitrary
  Python objects.

Hierarchical configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* `Hydra <https://hydra.cc/>`_ / `OmegaConf
  <https://omegaconf.readthedocs.io/>`_ -- composable, hierarchical YAML
  configuration with command-line overrides.

Standard-library building blocks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* `dataclasses <https://docs.python.org/3/library/dataclasses.html>`_ and
  `attrs <https://pypi.org/project/attrs/>`_ -- the field-with-default class
  pattern (formalized by :pep:`681`) that ``kwconf``'s static typing surface
  mirrors.


.. |Pypi| image:: https://img.shields.io/pypi/v/kwconf.svg
    :target: https://pypi.python.org/pypi/kwconf

.. |PypiDownloads| image:: https://img.shields.io/pypi/dm/kwconf.svg
    :target: https://pypistats.org/packages/kwconf

.. |ReadTheDocs| image:: https://readthedocs.org/projects/kwconf/badge/?version=latest
    :target: http://kwconf.readthedocs.io/en/latest/

.. |GithubActions| image:: https://github.com/Erotemic/kwconf/actions/workflows/tests.yml/badge.svg
    :target: https://github.com/Erotemic/kwconf/actions?query=branch%3Amain

.. |Codecov| image:: https://codecov.io/github/Erotemic/kwconf/badge.svg?branch=main&service=github
    :target: https://codecov.io/github/Erotemic/kwconf?branch=main

.. |GitlabCIPipeline| image:: https://gitlab.kitware.com/Erotemic/kwconf/badges/main/pipeline.svg
    :target: https://gitlab.kitware.com/Erotemic/kwconf/-/jobs

.. |GitlabCICoverage| image:: https://gitlab.kitware.com/Erotemic/kwconf/badges/main/coverage.svg
    :target: https://gitlab.kitware.com/Erotemic/kwconf/commits/main
