diff --git a/docs/usage.md b/docs/usage.md index 7f5c58b..12abff7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -75,3 +75,73 @@ config = ProjectConfig( print(config) # ProjectConfig (config_path=/some/other/path/pyproject.toml) ``` + +# Validation + +`maison` offers optional schema validation using [pydantic](https://pydantic-docs.helpmanual.io/). + +To validate a configuration, first create a schema which subclasses `ConfigSchema`: + +```python +from maison import ConfigSchema + +class MySchema(ConfigSchema): + foo: str = "my_default" +``` + +!!! note "" + `ConfigSchema` offers all the same functionality as the `pydantic` [BaseModel](https://pydantic-docs.helpmanual.io/usage/models/) + +Then inject the schema when instantiating a `ProjectConfig`: + +```python +from maison import ProjectConfig + +config = ProjectConfig(project_name="acme", schema=MySchema) +``` + +To validate the config, simply run `validate()` on the config instance: + +```python +config.validate() +``` + +If the configuration is invalid, a `pydantic` `ValidationError` will be raised. If the +configuration is valid, nothing will happen. + +## Casting and default values + +By default, `maison` will replace the values in the config with whatever comes back from +the validation. For example, for a config file that looks like this: + +```toml +[tool.acme] +foo = 1 +``` + +And a schema that looks like this: + +```python +class MySchema(ConfigSchema): + foo: str + bar: str = "my_default" +``` + +Running the config through validation will render the following: + +```python +config = ProjectConfig(project_name="acme", schema=MySchema) + +config.to_dict() # {"foo": 1} + +config.validate() +config.to_dict() # {"foo": "1", "bar": "my_default"} +``` + +If you prefer to keep the config values untouched and just perform simple validation, +add a `use_schema_values=False` argument to the `validate` method. + +## Schema precedence + +The `validate` method also accepts a `schema` is an argument. If one is provided here, +it will be used instead of a schema passed as an init argument. diff --git a/poetry.lock b/poetry.lock index 7baf4c6..58c1e33 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,88 +1,86 @@ [[package]] -category = "dev" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = "*" -version = "1.4.4" [[package]] -category = "dev" -description = "Utilities for refactoring imports in python-like syntax." name = "aspy.refactor-imports" +version = "2.2.0" +description = "Utilities for refactoring imports in python-like syntax." +category = "dev" optional = false python-versions = ">=3.6.1" -version = "2.2.0" [package.dependencies] cached-property = "*" [[package]] -category = "dev" -description = "Atomic file writes." -marker = "sys_platform == \"win32\"" name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.0" [[package]] -category = "dev" -description = "Classes Without Boilerplate" name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "21.2.0" [package.extras] -dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] -category = "dev" -description = "Compatibility shim providing selectable entry points for older implementations" name = "backports.entry-points-selectable" +version = "1.1.0" +description = "Compatibility shim providing selectable entry points for older implementations" +category = "dev" optional = false python-versions = ">=2.7" -version = "1.1.0" [package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] [[package]] -category = "dev" -description = "Security oriented static analyser for python code." name = "bandit" +version = "1.7.0" +description = "Security oriented static analyser for python code." +category = "dev" optional = false python-versions = ">=3.5" -version = "1.7.0" [package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} GitPython = ">=1.0.1" PyYAML = ">=5.3.1" -colorama = ">=0.3.9" six = ">=1.10.0" stevedore = ">=1.20.0" [[package]] -category = "dev" -description = "The uncompromising code formatter." name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.6" -version = "20.8b1" [package.dependencies] appdirs = "*" click = ">=7.1.2" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} mypy-extensions = ">=0.4.3" pathspec = ">=0.6,<1" regex = ">=2020.1.8" @@ -90,130 +88,118 @@ toml = ">=0.10.1" typed-ast = ">=1.4.0" typing-extensions = ">=3.7.4" -[package.dependencies.dataclasses] -python = "<3.7" -version = ">=0.6" - [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] -category = "dev" -description = "A decorator for caching properties in classes." name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +category = "dev" optional = false python-versions = "*" -version = "1.5.2" [[package]] -category = "dev" -description = "Python package for providing Mozilla's CA Bundle." name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" optional = false python-versions = "*" -version = "2021.10.8" [[package]] -category = "dev" -description = "Validate configuration and produce human readable error messages." name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.6.1" -version = "3.3.1" [[package]] -category = "dev" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -marker = "python_version >= \"3\"" name = "charset-normalizer" +version = "2.0.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" optional = false python-versions = ">=3.5.0" -version = "2.0.7" [package.extras] unicode_backport = ["unicodedata2"] [[package]] -category = "main" -description = "Composable command line interface toolkit" name = "click" +version = "8.0.3" +description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=3.6" -version = "8.0.3" [package.dependencies] -colorama = "*" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] -category = "main" -description = "Cross-platform colored terminal text." -marker = "platform_system == \"Windows\" or sys_platform == \"win32\" or platform_system == \"Windows\"" name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.4" [[package]] -category = "dev" -description = "Code coverage measurement for Python" name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.5" [package.dependencies] -[package.dependencies.toml] -optional = true -version = "*" +toml = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["toml"] [[package]] -category = "dev" -description = "A utility for ensuring Google-style docstrings stay up to date with the source code." name = "darglint" +version = "1.8.1" +description = "A utility for ensuring Google-style docstrings stay up to date with the source code." +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "1.8.1" [[package]] -category = "dev" -description = "A backport of the dataclasses module for Python 3.6" -marker = "python_version < \"3.7\"" name = "dataclasses" +version = "0.6" +description = "A backport of the dataclasses module for Python 3.6" +category = "main" optional = false python-versions = "*" -version = "0.6" [[package]] -category = "dev" -description = "Distribution utilities" name = "distlib" +version = "0.3.3" +description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" -version = "0.3.3" [[package]] -category = "dev" -description = "Docutils -- Python Documentation Utilities" name = "docutils" +version = "0.17.1" +description = "Docutils -- Python Documentation Utilities" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.17.1" [[package]] -category = "dev" -description = "A parser for Python dependency files" name = "dparse" +version = "0.5.1" +description = "A parser for Python dependency files" +category = "dev" optional = false python-versions = ">=3.5" -version = "0.5.1" [package.dependencies] packaging = "*" @@ -224,41 +210,38 @@ toml = "*" pipenv = ["pipenv"] [[package]] -category = "dev" -description = "A platform independent file lock." name = "filelock" +version = "3.3.1" +description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.6" -version = "3.3.1" [package.extras] docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] [[package]] -category = "dev" -description = "the modular source code checker: pep8 pyflakes and co" name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "3.9.2" [package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - [[package]] -category = "dev" -description = "Automated security testing with bandit and flake8." name = "flake8-bandit" +version = "2.1.2" +description = "Automated security testing with bandit and flake8." +category = "dev" optional = false python-versions = "*" -version = "2.1.2" [package.dependencies] bandit = "*" @@ -267,12 +250,12 @@ flake8-polyfill = "*" pycodestyle = "*" [[package]] -category = "dev" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." name = "flake8-bugbear" +version = "21.9.2" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" optional = false python-versions = ">=3.6" -version = "21.9.2" [package.dependencies] attrs = ">=19.2.0" @@ -282,35 +265,35 @@ flake8 = ">=3.0.0" dev = ["coverage", "black", "hypothesis", "hypothesmith"] [[package]] -category = "dev" -description = "Extension for flake8 which uses pydocstyle to check docstrings" name = "flake8-docstrings" +version = "1.6.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +category = "dev" optional = false python-versions = "*" -version = "1.6.0" [package.dependencies] flake8 = ">=3" pydocstyle = ">=2.1" [[package]] -category = "dev" -description = "Polyfill package for Flake8 plugins" name = "flake8-polyfill" +version = "1.0.2" +description = "Polyfill package for Flake8 plugins" +category = "dev" optional = false python-versions = "*" -version = "1.0.2" [package.dependencies] flake8 = "*" [[package]] -category = "dev" -description = "Python docstring reStructuredText (RST) validator" name = "flake8-rst-docstrings" +version = "0.2.3" +description = "Python docstring reStructuredText (RST) validator" +category = "dev" optional = false python-versions = ">=3.3" -version = "0.2.3" [package.dependencies] flake8 = ">=3.0.0" @@ -318,270 +301,264 @@ pygments = "*" restructuredtext-lint = "*" [[package]] -category = "dev" -description = "Git Object Database" name = "gitdb" +version = "4.0.9" +description = "Git Object Database" +category = "dev" optional = false python-versions = ">=3.6" -version = "4.0.9" [package.dependencies] smmap = ">=3.0.1,<6" [[package]] -category = "dev" -description = "Python Git Library" name = "gitpython" +version = "3.1.20" +description = "Python Git Library" +category = "dev" optional = false python-versions = ">=3.6" -version = "3.1.20" [package.dependencies] gitdb = ">=4.0.1,<5" - -[package.dependencies.typing-extensions] -python = "<3.10" -version = ">=3.7.4.3" +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} [[package]] -category = "dev" -description = "File identification library for Python" name = "identify" +version = "2.3.1" +description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.6.1" -version = "2.3.1" [package.extras] license = ["editdistance-s"] [[package]] -category = "dev" -description = "Internationalized Domain Names in Applications (IDNA)" -marker = "python_version >= \"3\"" name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" optional = false python-versions = ">=3.5" -version = "3.3" [[package]] -category = "main" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" name = "importlib-metadata" +version = "4.8.1" +description = "Read metadata from Python packages" +category = "main" optional = false python-versions = ">=3.6" -version = "4.8.1" [package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" -[package.dependencies.typing-extensions] -python = "<3.8" -version = ">=3.6.4" - [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] -category = "dev" -description = "Read resources from Python packages" -marker = "python_version < \"3.7\"" name = "importlib-resources" +version = "5.3.0" +description = "Read resources from Python packages" +category = "dev" optional = false python-versions = ">=3.6" -version = "5.3.0" [package.dependencies] -[package.dependencies.zipp] -python = "<3.10" -version = ">=3.1.0" +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] [[package]] -category = "dev" -description = "iniconfig: brain-dead simple config-ini parsing" name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = "*" -version = "1.1.1" [[package]] -category = "dev" -description = "McCabe checker, plugin for flake8" name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" optional = false python-versions = "*" -version = "0.6.1" [[package]] -category = "dev" -description = "Optional static typing for Python" name = "mypy" +version = "0.910" +description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.5" -version = "0.910" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" toml = "*" +typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} typing-extensions = ">=3.7.4" -[package.dependencies.typed-ast] -python = "<3.8" -version = ">=1.4.0,<1.5.0" - [package.extras] dmypy = ["psutil (>=4.0)"] python2 = ["typed-ast (>=1.4.0,<1.5.0)"] [[package]] -category = "dev" -description = "Experimental type system extensions for programs checked with the mypy typechecker." name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" optional = false python-versions = "*" -version = "0.4.3" [[package]] -category = "dev" -description = "Node.js virtual environment builder" name = "nodeenv" +version = "1.6.0" +description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = "*" -version = "1.6.0" [[package]] -category = "dev" -description = "Core utilities for Python packages" name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "dev" optional = false python-versions = ">=3.6" -version = "21.0" [package.dependencies] pyparsing = ">=2.0.2" [[package]] -category = "dev" -description = "Utility library for gitignore style pattern matching of file paths." name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "0.9.0" [[package]] -category = "dev" -description = "Python Build Reasonableness" name = "pbr" +version = "5.6.0" +description = "Python Build Reasonableness" +category = "dev" optional = false python-versions = ">=2.6" -version = "5.6.0" [[package]] -category = "dev" -description = "Check PEP-8 naming conventions, plugin for flake8" name = "pep8-naming" +version = "0.12.1" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" optional = false python-versions = "*" -version = "0.12.1" [package.dependencies] flake8 = ">=3.9.1" flake8-polyfill = ">=1.0.2,<2" [[package]] -category = "dev" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." name = "platformdirs" +version = "2.4.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.6" -version = "2.4.0" [package.extras] docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] -category = "dev" -description = "plugin and hook calling mechanisms for python" name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.6" -version = "1.0.0" [package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] -category = "dev" -description = "A framework for managing and maintaining multi-language pre-commit hooks." name = "pre-commit" +version = "2.15.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.6.1" -version = "2.15.0" [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-resources = {version = "*", markers = "python_version < \"3.7\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - -[package.dependencies.importlib-resources] -python = "<3.7" -version = "*" - [[package]] -category = "dev" -description = "Some out-of-the-box hooks for pre-commit." name = "pre-commit-hooks" +version = "4.0.1" +description = "Some out-of-the-box hooks for pre-commit." +category = "dev" optional = false python-versions = ">=3.6.1" -version = "4.0.1" [package.dependencies] "ruamel.yaml" = ">=0.15" toml = "*" [[package]] -category = "dev" -description = "library with cross-python path, ini-parsing, io, code, log facilities" name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.10.0" [[package]] -category = "dev" -description = "Python style guide checker" name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.7.0" [[package]] -category = "dev" -description = "Python docstring style checker" +name = "pydantic" +version = "1.8.2" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] name = "pydocstyle" +version = "6.1.1" +description = "Python docstring style checker" +category = "dev" optional = false python-versions = ">=3.6" -version = "6.1.1" [package.dependencies] snowballstemmer = "*" @@ -590,302 +567,274 @@ snowballstemmer = "*" toml = ["toml"] [[package]] -category = "dev" -description = "passive checker of Python programs" name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.3.1" [[package]] -category = "dev" -description = "Pygments is a syntax highlighting package written in Python." name = "pygments" +version = "2.10.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" optional = false python-versions = ">=3.5" -version = "2.10.0" [[package]] -category = "dev" -description = "Python parsing module" name = "pyparsing" +version = "3.0.1" +description = "Python parsing module" +category = "dev" optional = false python-versions = ">=3.6" -version = "3.0.1" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] [[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.6" -version = "6.2.5" [package.dependencies] -atomicwrites = ">=1.0" +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" -colorama = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] -category = "dev" -description = "YAML parser and emitter for Python" name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" optional = false python-versions = ">=3.6" -version = "6.0" [[package]] -category = "dev" -description = "Alternative regular expression module, to replace re." name = "regex" +version = "2021.10.23" +description = "Alternative regular expression module, to replace re." +category = "dev" optional = false python-versions = "*" -version = "2021.10.23" [[package]] -category = "dev" -description = "Tool for reordering python imports" name = "reorder-python-imports" +version = "2.6.0" +description = "Tool for reordering python imports" +category = "dev" optional = false python-versions = ">=3.6.1" -version = "2.6.0" [package.dependencies] "aspy.refactor-imports" = ">=2.1.0" [[package]] -category = "dev" -description = "Python HTTP for Humans." name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -version = "2.26.0" [package.dependencies] certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" -[package.dependencies.charset-normalizer] -python = ">=3" -version = ">=2.0.0,<2.1.0" - -[package.dependencies.idna] -python = ">=3" -version = ">=2.5,<4" - [package.extras] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] -category = "dev" -description = "reStructuredText linter" name = "restructuredtext-lint" +version = "1.3.2" +description = "reStructuredText linter" +category = "dev" optional = false python-versions = "*" -version = "1.3.2" [package.dependencies] docutils = ">=0.11,<1.0" [[package]] -category = "dev" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" name = "ruamel.yaml" +version = "0.17.16" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "dev" optional = false python-versions = ">=3" -version = "0.17.16" [package.dependencies] -[package.dependencies."ruamel.yaml.clib"] -python = "<3.10" -version = ">=0.1.2" +"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""} [package.extras] docs = ["ryd"] jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [[package]] -category = "dev" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -marker = "platform_python_implementation == \"CPython\" and python_version < \"3.10\"" name = "ruamel.yaml.clib" +version = "0.2.6" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "dev" optional = false python-versions = ">=3.5" -version = "0.2.6" [[package]] -category = "dev" -description = "Checks installed dependencies for known vulnerabilities." name = "safety" +version = "1.10.3" +description = "Checks installed dependencies for known vulnerabilities." +category = "dev" optional = false python-versions = ">=3.5" -version = "1.10.3" [package.dependencies] Click = ">=6.0" dparse = ">=0.5.1" packaging = "*" requests = "*" -setuptools = "*" [[package]] -category = "dev" -description = "Python 2 and 3 compatibility utilities" name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.16.0" [[package]] -category = "dev" -description = "A pure Python implementation of a sliding window memory map manager" name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" optional = false python-versions = ">=3.6" -version = "5.0.0" [[package]] -category = "dev" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." name = "snowballstemmer" +version = "2.1.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" optional = false python-versions = "*" -version = "2.1.0" [[package]] -category = "dev" -description = "Manage dynamic plugins for Python applications" name = "stevedore" +version = "3.5.0" +description = "Manage dynamic plugins for Python applications" +category = "dev" optional = false python-versions = ">=3.6" -version = "3.5.0" [package.dependencies] +importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} pbr = ">=2.0.0,<2.1.0 || >2.1.0" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=1.7.0" - [[package]] -category = "dev" -description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.10.2" [[package]] -category = "dev" -description = "a fork of Python 2 and 3 ast modules with type comment support" name = "typed-ast" +version = "1.4.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" optional = false python-versions = "*" -version = "1.4.3" [[package]] -category = "dev" -description = "Run-time type checker for Python" name = "typeguard" +version = "2.13.0" +description = "Run-time type checker for Python" +category = "dev" optional = false python-versions = ">=3.5.3" -version = "2.13.0" [package.extras] doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["pytest", "typing-extensions", "mypy"] [[package]] -category = "dev" -description = "Typing stubs for toml" name = "types-toml" +version = "0.10.1" +description = "Typing stubs for toml" +category = "dev" optional = false python-versions = "*" -version = "0.10.1" [[package]] -category = "main" -description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" optional = false python-versions = "*" -version = "3.10.0.2" [[package]] -category = "dev" -description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.26.7" [package.extras] brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] -category = "dev" -description = "Virtual Python Environment builder" name = "virtualenv" +version = "20.9.0" +description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "20.9.0" [package.dependencies] "backports.entry-points-selectable" = ">=1.0.4" distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} platformdirs = ">=2,<3" six = ">=1.9.0,<2" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - -[package.dependencies.importlib-resources] -python = "<3.7" -version = ">=1.0" - [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] [[package]] -category = "dev" -description = "A rewrite of the builtin doctest module" name = "xdoctest" +version = "0.15.10" +description = "A rewrite of the builtin doctest module" +category = "dev" optional = false python-versions = "*" -version = "0.15.10" [package.dependencies] +colorama = {version = "*", optional = true, markers = "platform_system == \"Windows\" and extra == \"colors\""} +Pygments = {version = "*", optional = true, markers = "extra == \"colors\""} six = "*" -[package.dependencies.Pygments] -optional = true -version = "*" - -[package.dependencies.colorama] -optional = true -version = "*" - [package.extras] all = ["six", "codecov", "scikit-build", "cmake", "ninja", "pybind11", "pygments", "colorama", "pytest", "pytest", "pytest-cov", "pytest", "pytest", "pytest-cov", "typing", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel", "pytest", "pytest-cov"] colors = ["pygments", "colorama"] @@ -894,22 +843,21 @@ optional = ["pygments", "colorama", "nbformat", "nbconvert", "jupyter-client", " tests = ["codecov", "scikit-build", "cmake", "ninja", "pybind11", "pytest", "pytest", "pytest-cov", "pytest", "pytest", "pytest-cov", "typing", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel", "pytest", "pytest-cov"] [[package]] -category = "main" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" optional = false python-versions = ">=3.6" -version = "3.6.0" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] -content-hash = "4dff7853f156825bae73d6ceca3284caa9eb02894e4b748b2cf00220f65a4e3e" -lock-version = "1.0" +lock-version = "1.1" python-versions = "^3.6.1" +content-hash = "6985588fe2f184df65444747222242013b2fd5fcf98057b67220fd2d5fbd2e19" [metadata.files] appdirs = [ @@ -1169,6 +1117,30 @@ pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] +pydantic = [ + {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, + {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, + {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, + {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, + {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, + {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, + {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, + {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, + {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, + {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, +] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, diff --git a/pyproject.toml b/pyproject.toml index 82a8353..7d9b434 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maison" -version = "1.1.0" +version = "1.2.0" description = "Maison" authors = ["Dom Batten "] license = "MIT" @@ -22,6 +22,7 @@ Changelog = "https://github.com/dbatten5/maison/releases" [tool.poetry.dependencies] python = "^3.6.1" click = "^8.0.1" +pydantic = "^1.8.2" [tool.poetry.dev-dependencies] pytest = "^6.2.4" diff --git a/src/maison/__init__.py b/src/maison/__init__.py index 54c6e4d..5ebfeb9 100644 --- a/src/maison/__init__.py +++ b/src/maison/__init__.py @@ -1,2 +1,3 @@ """Maison.""" from .config import ProjectConfig +from .schema import ConfigSchema diff --git a/src/maison/config.py b/src/maison/config.py index 3746638..7aa9e31 100644 --- a/src/maison/config.py +++ b/src/maison/config.py @@ -4,7 +4,9 @@ from typing import Dict from typing import List from typing import Optional +from typing import Type +from maison.schema import ConfigSchema from maison.utils import _find_config @@ -16,6 +18,7 @@ def __init__( project_name: str, starting_path: Optional[Path] = None, source_files: Optional[List[str]] = None, + schema: Optional[Type[ConfigSchema]] = None, ) -> None: """Initialize the config. @@ -26,6 +29,7 @@ def __init__( file source_files: an optional list of source config filenames to search for. If none is provided then `pyproject.toml` will be used + schema: an optional `pydantic` model to define the config schema """ self.source_files = source_files or ["pyproject.toml"] config_path, config_dict = _find_config( @@ -35,6 +39,7 @@ def __init__( ) self._config_dict: Dict[str, Any] = config_dict or {} self.config_path: Optional[Path] = config_path + self.schema = schema def __repr__(self) -> str: """Return the __repr__. @@ -60,6 +65,45 @@ def to_dict(self) -> Dict[str, Any]: """ return self._config_dict + def validate( + self, + schema: Optional[Type[ConfigSchema]] = None, + use_schema_values: bool = True, + ) -> None: + """Validate the configuration. + + Note that this will cast values to whatever is defined in the schema. For + example, for the following schema: + + class Schema(ConfigSchema): + foo: str + + Validating a config with: + + {"foo": 1} + + Will result in: + + {"foo": "1"} + + Args: + schema: an optional `pydantic` base model to define the schema. This takes + precedence over a schema provided at object instantiation. + use_schema_values: an optional boolean to indicate whether the result + of passing the config through the schema should overwrite the existing + config values, meaning values are cast to types defined in the schema as + described above, and default values defined in the schema are used. + """ + validated_schema: Optional[ConfigSchema] = None + + if schema: + validated_schema = schema(**self._config_dict) + elif self.schema: + validated_schema = self.schema(**self._config_dict) + + if validated_schema and use_schema_values: + self._config_dict = validated_schema.dict() + def get_option( self, option_name: str, default_value: Optional[Any] = None ) -> Optional[Any]: diff --git a/src/maison/schema.py b/src/maison/schema.py new file mode 100644 index 0000000..bb6605d --- /dev/null +++ b/src/maison/schema.py @@ -0,0 +1,6 @@ +"""Module to define the `ConfigSchema` class.""" +from pydantic import BaseModel + + +class ConfigSchema(BaseModel): + """A class for creating schemas based on the `pydantic` `BaseModel`.""" diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 7321b73..83a8c5d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -2,9 +2,12 @@ from pathlib import Path from typing import Callable +import pytest import toml +from pydantic import ValidationError from maison.config import ProjectConfig +from maison.schema import ConfigSchema class TestProjectConfig: @@ -198,3 +201,165 @@ def test_valid_ini_file(self, create_tmp_file: Callable[..., Path]) -> None: "section 1": {"option_1": "value_1"}, "section 2": {"option_2": "value_2"}, } + + +class TestValidation: + """Tests for schema validation.""" + + def test_no_schema(self) -> None: + """ + Given an instance of `ProjectConfig` with no schema, + When the `validate` method is called, + Then nothing happens + """ + config = ProjectConfig(project_name="acme", starting_path=Path("/")) + + assert config.to_dict() == {} + + config.validate() + + assert config.to_dict() == {} + + def test_one_schema_with_valid_config( + self, + create_tmp_file: Callable[..., Path], + ) -> None: + """ + Given an instance of `ProjectConfig` with a given schema, + When the `validate` method is called, + Then the configuration is validated + """ + + class Schema(ConfigSchema): + """Defines schema.""" + + bar: str + + config_toml = toml.dumps({"tool": {"foo": {"bar": "baz"}}}) + pyproject_path = create_tmp_file(content=config_toml, filename="pyproject.toml") + config = ProjectConfig( + project_name="foo", + starting_path=pyproject_path, + schema=Schema, + ) + + config.validate() + + assert config.get_option("bar") == "baz" + + def test_use_schema_values( + self, + create_tmp_file: Callable[..., Path], + ) -> None: + """ + Given an instance of `ProjectConfig` with a given schema, + When the `validate` method is called, + Then the configuration is validated and values are cast to those in the schema + and default values are used + """ + + class Schema(ConfigSchema): + """Defines schema.""" + + bar: str + other: str = "hello" + + config_toml = toml.dumps({"tool": {"foo": {"bar": 1}}}) + pyproject_path = create_tmp_file(content=config_toml, filename="pyproject.toml") + config = ProjectConfig( + project_name="foo", + starting_path=pyproject_path, + schema=Schema, + ) + + config.validate() + + assert config.get_option("bar") == "1" + assert config.get_option("other") == "hello" + + def test_not_use_schema_values( + self, + create_tmp_file: Callable[..., Path], + ) -> None: + """ + Given an instance of `ProjectConfig` with a given schema, + When the `validate` method is called with `use_schema_values` set to `False`, + Then the configuration is validated but values remain as in the config + """ + + class Schema(ConfigSchema): + """Defines schema.""" + + bar: str + other: str = "hello" + + config_toml = toml.dumps({"tool": {"foo": {"bar": 1}}}) + pyproject_path = create_tmp_file(content=config_toml, filename="pyproject.toml") + config = ProjectConfig( + project_name="foo", + starting_path=pyproject_path, + schema=Schema, + ) + + config.validate(use_schema_values=False) + + assert config.get_option("bar") == 1 + assert config.get_option("other") is None + + def test_schema_override( + self, + create_tmp_file: Callable[..., Path], + ) -> None: + """ + Given an instance of `ProjectConfig` with a given schema, + When the `validate` method is called with a new schema, + Then the new schema is used + """ + + class Schema1(ConfigSchema): + """Defines schema for 1.""" + + bar: str = "schema_1" + + class Schema2(ConfigSchema): + """Defines schema for 2.""" + + bar: str = "schema_2" + + config_toml = toml.dumps({"tool": {"foo": {}}}) + pyproject_path = create_tmp_file(content=config_toml, filename="pyproject.toml") + config = ProjectConfig( + project_name="foo", + starting_path=pyproject_path, + schema=Schema1, + ) + + config.validate(schema=Schema2) + + assert config.get_option("bar") == "schema_2" + + def test_invalid_configuration( + self, + create_tmp_file: Callable[..., Path], + ) -> None: + """ + Given a configuration which doesn't conform to the schema, + When the `validate` method is called, + Then an error is raised + """ + + class Schema(ConfigSchema): + """Defines schema.""" + + bar: str + + config_toml = toml.dumps({"tool": {"foo": {}}}) + pyproject_path = create_tmp_file(content=config_toml, filename="pyproject.toml") + config = ProjectConfig( + project_name="foo", + starting_path=pyproject_path, + schema=Schema, + ) + + with pytest.raises(ValidationError): + config.validate()