You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

envier

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

envier - pypi Package Compare versions

Comparing version
0.2.1
to
0.3.0
+93
envier/sphinx.py
import sys
from docutils import nodes
from docutils.frontend import OptionParser
from docutils.parsers.rst import Directive
from docutils.parsers.rst import Parser
from docutils.utils import new_document
def asbool(argument):
return argument.lower() in {"yes", "true", "t", "1", "y", "on"}
RST_PARSER = Parser()
RST_PARSER_SETTINGS = OptionParser(components=(Parser,)).get_default_values()
def _parse(cell):
doc = new_document("", RST_PARSER_SETTINGS)
RST_PARSER.parse(cell, doc)
return doc.children
def _create_row(cells):
row = nodes.row()
for cell in cells:
entry = nodes.entry()
entry += _parse(cell)
row += entry
return row
class Envier(Directive):
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {
"heading": asbool,
"recursive": asbool,
}
def run(self):
module_name, _, config_class = self.arguments[0].partition(":")
__import__(module_name)
module = sys.modules[module_name]
config_spec = None
for part in config_class.split("."):
config_spec = getattr(module, part)
if config_spec is None:
raise ValueError(
"Could not find configuration spec class {} from {}".format(
config_class, module_name
)
)
has_header = self.options.get("heading", True)
recursive = self.options.get("recursive", False)
table = nodes.table()
table["classes"] += ["colwidths-given"]
# Column specs
tgroup = nodes.tgroup(cols=4)
table += tgroup
for col_width in (3, 1, 1, 4): # TODO: make it configurable?
tgroup += nodes.colspec(colwidth=col_width)
head = ("Variable Name", "Type", "Default Value", "Description")
# Table heading
if has_header:
thead = nodes.thead()
thead += _create_row(head)
tgroup += thead
# Table body
tbody = nodes.tbody()
tgroup += tbody
for row in config_spec.help_info(recursive=recursive):
tbody += _create_row(row)
return [table]
def setup(app):
app.add_directive("envier", Envier)
return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
import sys
if sys.version_info >= (3, 6):
# DEV: We are not running the Sphinx extension tests on Python < 3.6 due to
# lack of support.
import pytest
from sphinx.testing.path import path
pytest_plugins = "sphinx.testing.fixtures"
@pytest.fixture(scope="session")
def rootdir():
return path(__file__).parent.abspath() / "sphinx"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.18.1: http://docutils.sourceforge.net/" />
<title>&lt;no title&gt; &#8212; test-root documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<table class="docutils align-default">
<colgroup>
<col style="width: 33%" />
<col style="width: 11%" />
<col style="width: 11%" />
<col style="width: 44%" />
</colgroup>
<thead>
<tr class="row-odd"><th class="head"><p>Variable Name</p></th>
<th class="head"><p>Type</p></th>
<th class="head"><p>Default Value</p></th>
<th class="head"><p>Description</p></th>
</tr>
</thead>
<tbody>
<tr class="row-even"><td><p><code class="docutils literal notranslate"><span class="pre">MYAPP_DEBUG</span></code></p></td>
<td><p>Boolean</p></td>
<td><p>False</p></td>
<td><p>Whether to enable debug logging.</p></td>
</tr>
<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span class="pre">MYAPP_NO_DEFAULT</span></code></p></td>
<td><p>Boolean</p></td>
<td></td>
<td><p>A variable with no default value, which makes it mandatory.</p></td>
</tr>
<tr class="row-even"><td><p><code class="docutils literal notranslate"><span class="pre">MYAPP_URL</span></code></p></td>
<td><p>String</p></td>
<td><p><a class="reference external" href="http://localhost:5000">http://localhost:5000</a></p></td>
<td><p>The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application.</p></td>
</tr>
<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span class="pre">MYAPP_SERVICE_HOST</span></code></p></td>
<td><p><code class="docutils literal notranslate"><span class="pre">str</span></code></p></td>
<td><p>localhost</p></td>
<td><p>The host of the service.</p></td>
</tr>
<tr class="row-even"><td><p><code class="docutils literal notranslate"><span class="pre">MYAPP_SERVICE_PORT</span></code></p></td>
<td><p><code class="docutils literal notranslate"><span class="pre">int</span></code></p></td>
<td><p>3000</p></td>
<td><p>The port of the service.</p></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="#">test-root</a></h1>
<h3>Navigation</h3>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="#">Documentation overview</a><ul>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>document.getElementById('searchbox').style.display = "block"</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="footer">
&copy;2022, Datadog.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 5.0.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
<a href="_sources/index.rst.txt"
rel="nofollow">Page source</a>
</div>
</body>
</html>
# 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
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = "test-root"
copyright = "2022, Datadog"
author = "Datadog"
# -- 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 = [
"envier.sphinx",
]
# 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"]
# -- 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 = "alabaster"
# 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"]
.. envier:: tests.test_help:GlobalConfig
:heading: true
:recursive: true
from envier import Env
class GlobalConfig(Env):
__prefix__ = "myapp"
debug_mode = Env.var(
bool,
"debug",
default=False,
help_type="Boolean",
help="Whether to enable debug logging",
)
url = Env.var(
str,
"url",
default="http://localhost:5000",
help_type="String",
help="The URL of the application. " * 10,
)
no_default = Env.var(
str,
"no_default",
help_type="Boolean",
help="A variable with no default value, which makes it mandatory",
)
class ServiceConfig(Env):
__item__ = __prefix__ = "service"
host = Env.var(
str,
"host",
default="localhost",
help="The host of the service.",
)
port = Env.var(
int,
"port",
default=3000,
help="The port of the service.",
)
def test_help_info(monkeypatch):
monkeypatch.setenv("MYAPP_NO_DEFAULT", "1")
assert GlobalConfig.help_info() == [
("``MYAPP_DEBUG``", "Boolean", "False", "Whether to enable debug logging."),
(
"``MYAPP_NO_DEFAULT``",
"Boolean",
"",
"A variable with no default value, which makes it mandatory.",
),
(
"``MYAPP_URL``",
"String",
"http://localhost:5000",
"The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application.",
),
]
def test_help_info_recursive(monkeypatch):
monkeypatch.setenv("MYAPP_NO_DEFAULT", "1")
assert GlobalConfig.help_info(recursive=True) == [
("``MYAPP_DEBUG``", "Boolean", "False", "Whether to enable debug logging."),
(
"``MYAPP_NO_DEFAULT``",
"Boolean",
"",
"A variable with no default value, which makes it mandatory.",
),
(
"``MYAPP_URL``",
"String",
"http://localhost:5000",
"The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application. The URL of the application.",
),
("``MYAPP_SERVICE_HOST``", "``str``", "localhost", "The host of the service."),
("``MYAPP_SERVICE_PORT``", "``int``", "3000", "The port of the service."),
]
from filecmp import cmp
from sys import version_info as PY
import pytest
@pytest.mark.skipif(PY < (3, 6), reason="requires Python 3.6+")
def test(app, rootdir):
app.build()
reference = rootdir / "test-root" / "_build" / "index.html"
generated = app.outdir / "index.html"
assert cmp(reference, generated)
+22
-1
Metadata-Version: 2.1
Name: envier
Version: 0.2.1
Version: 0.3.0
Summary: Python application configuration via the environment

@@ -114,2 +114,23 @@ Home-page: https://github.com/DataDog/envier

## Sphinx Plugin
The library comes with a Sphinx plugin at `envier.sphinx` to allow generating
documentation from the configuration spec class directly. It exposes the
``envier`` directive that takes a mandatory argument, the configuration spec
class in the form `module:class`; additionally, the options `heading` and
`recursive` can be used to control whether to add heading and whether to
recursively get help information from nested configuration spec classes
respectively. By default, the plugin will display the table heading and will not
recurse over nested configuration spec classes.
Here is an example for a configuration class `GlobalConfig` located in the
`myapp.config` module. We omit the table header and recurse over nested
configuration.
~~~ rst
.. envier:: myapp.config:GlobalConfig
:heading: false
:recursive: true
~~~
## Roadmap

@@ -116,0 +137,0 @@

@@ -13,2 +13,3 @@ .gitignore

envier/mypy.py
envier/sphinx.py
envier.egg-info/PKG-INFO

@@ -20,4 +21,11 @@ envier.egg-info/SOURCES.txt

envier.egg-info/top_level.txt
tests/__init__.py
tests/conftest.py
tests/test_env.py
tests/test_help.py
tests/test_sphinx.py
tests/test_types.py
tests/types_test.py
tests/types_test.py
tests/sphinx/test-root/conf.py
tests/sphinx/test-root/index.rst
tests/sphinx/test-root/_build/index.html

@@ -18,3 +18,4 @@ import os

class NoDefaultType(object):
pass
def __str__(self):
return ""

@@ -31,2 +32,3 @@

MapType = Union[Callable[[str], V], Callable[[str, str], Tuple[K, V]]]
HelpInfo = Tuple[str, str, str, str]

@@ -57,2 +59,5 @@

deprecations=None, # type: Optional[List[DeprecationInfo]]
help=None, # type: Optional[str]
help_type=None, # type: Optional[str]
help_default=None, # type: Optional[str]
):

@@ -67,2 +72,3 @@ # type: (...) -> None

raise TypeError("default must be of type {}".format(type))
self.type = type

@@ -76,2 +82,6 @@ self.name = name

self.help = help
self.help_type = help_type
self.help_default = help_default
def _retrieve(self, env, prefix):

@@ -235,3 +245,3 @@ # type: (Env, str) -> T

elif isinstance(e, type) and issubclass(e, Env):
if e.__item__ is not None:
if e.__item__ is not None and e.__item__ != name:
# Move the subclass to the __item__ attribute

@@ -258,5 +268,19 @@ setattr(self.spec, e.__item__, e)

deprecations=None, # type: Optional[List[DeprecationInfo]]
help=None, # type: Optional[str]
help_type=None, # type: Optional[str]
help_default=None, # type: Optional[str]
):
# type: (...) -> EnvVariable[T]
return EnvVariable(type, name, parser, validator, map, default, deprecations)
return EnvVariable(
type,
name,
parser,
validator,
map,
default,
deprecations,
help,
help_type,
help_default,
)

@@ -273,5 +297,19 @@ @classmethod

deprecations=None, # type: Optional[List[DeprecationInfo]]
help=None, # type: Optional[str]
help_type=None, # type: Optional[str]
help_default=None, # type: Optional[str]
):
# type: (...) -> EnvVariable[T]
return EnvVariable(type, name, parser, validator, map, default, deprecations)
return EnvVariable(
type,
name,
parser,
validator,
map,
default,
deprecations,
help,
help_type,
help_default,
)

@@ -301,2 +339,14 @@ @classmethod

@classmethod
def values(cls):
# type: () -> Iterator[Union[EnvVariable, DerivedVariable, Type[Env]]]
"""Return the names of all the items."""
return (
v
for v in cls.__dict__.values()
if isinstance(v, (EnvVariable, DerivedVariable))
or isinstance(v, type)
and issubclass(v, Env)
)
@classmethod
def include(cls, env_spec, namespace=None, overwrite=False):

@@ -335,1 +385,62 @@ # type: (Type[Env], Optional[str], bool) -> None

setattr(cls, k, v)
@classmethod
def help_info(cls, recursive=False):
# type: (bool) -> List[HelpInfo]
"""Extract the help information from the class.
Returns a list of all the environment variables declared by the class.
The format of each entry is a tuple consisting of the variable name (in
double backtics quotes), the type, the default value, and the help text.
Set ``recursive`` to ``True`` to include variables from nested Env
classes.
"""
entries = []
def add_entries(full_prefix, config):
# type: (str, Type[Env]) -> None
vars = sorted(
(_ for _ in config.values() if isinstance(_, EnvVariable)),
key=lambda v: v.name,
)
for v in vars:
# Add a period at the end if necessary.
help_message = v.help.strip() if v.help is not None else ""
if help_message and not help_message.endswith("."):
help_message += "."
entries.append(
(
"``" + full_prefix + _normalized(v.name) + "``",
v.help_type or "``%s``" % v.type.__name__, # type: ignore[attr-defined]
v.help_default or str(v.default),
help_message,
)
)
configs = [("", cls)]
while configs:
full_prefix, config = configs.pop()
new_prefix = full_prefix + _normalized(config.__prefix__)
if not new_prefix.endswith("_"):
new_prefix += "_"
add_entries(new_prefix, config)
if not recursive:
break
subconfigs = sorted(
(
(new_prefix, v)
for k, v in config.__dict__.items()
if isinstance(v, type) and issubclass(v, Env) and k != "parent"
),
key=lambda _: _[1].__prefix__,
)
configs[0:0] = subconfigs # DFS
return entries
+20
-9

@@ -1,4 +0,2 @@

from typing import Callable
from typing import Optional
from typing import Type
import typing as t

@@ -9,2 +7,6 @@ from mypy.exprtotype import expr_to_unanalyzed_type

from mypy.nodes import ClassDef
from mypy.nodes import MemberExpr
from mypy.nodes import NameExpr
from mypy.nodes import StrExpr
from mypy.nodes import Var
from mypy.plugin import ClassDefContext

@@ -14,3 +16,6 @@ from mypy.plugin import MethodContext

from mypy.typeops import make_simplified_union
from mypy.types import FunctionLike
from mypy.types import Instance
from mypy.types import ProperType
from mypy.types import Type

@@ -26,3 +31,3 @@

def _envier_attr_callback(ctx):
# type: (MethodContext) -> Type
# type: (MethodContext) -> ProperType
arg_type = ctx.arg_types[0][0]

@@ -33,3 +38,4 @@ if isinstance(arg_type, Instance):

return make_simplified_union({_.ret_type for _ in arg_type.items})
assert isinstance(arg_type, FunctionLike), arg_type
return make_simplified_union({_.ret_type for _ in arg_type.items}) # type: ignore[arg-type]

@@ -45,3 +51,4 @@

or not isinstance(decl, CallExpr)
or decl.callee.expr.fullname not in _envier_base_classes
or t.cast(NameExpr, t.cast(MemberExpr, decl.callee).expr).fullname
not in _envier_base_classes
):

@@ -53,2 +60,3 @@ # We assume a single assignment per line, so this can't be an

(attr,) = stmt.lvalues
assert isinstance(attr, NameExpr) and isinstance(attr.node, Var), attr

@@ -74,2 +82,5 @@ attr.node.type = ctx.api.anal_type(

# The value of the __item__ attribute must be a string.
assert isinstance(s.rvalue, StrExpr), s.rvalue
# Move the statement over from the class name to the item name

@@ -81,3 +92,3 @@ ctx.cls.info.names[s.rvalue.value] = ctx.cls.info.names.pop(stmt.name)

def get_method_hook(self, fullname):
# type: (str) -> Optional[Callable[[MethodContext], Type]]
# type: (str) -> t.Optional[t.Callable[[MethodContext], Type]]
if fullname in _envier_attr_makers:

@@ -92,3 +103,3 @@ # We use this callback to override the the method return value to

def get_base_class_hook(self, fullname):
# type: (str) -> Optional[Callable[[ClassDefContext], None]]
# type: (str) -> t.Optional[t.Callable[[ClassDefContext], None]]
if fullname in _envier_base_classes:

@@ -103,3 +114,3 @@ # We use this callback to override the class attribute types to

def plugin(version):
# type: (str) -> Plugin
# type: (str) -> t.Type[EnvierPlugin]
return EnvierPlugin
Metadata-Version: 2.1
Name: envier
Version: 0.2.1
Version: 0.3.0
Summary: Python application configuration via the environment

@@ -114,2 +114,23 @@ Home-page: https://github.com/DataDog/envier

## Sphinx Plugin
The library comes with a Sphinx plugin at `envier.sphinx` to allow generating
documentation from the configuration spec class directly. It exposes the
``envier`` directive that takes a mandatory argument, the configuration spec
class in the form `module:class`; additionally, the options `heading` and
`recursive` can be used to control whether to add heading and whether to
recursively get help information from nested configuration spec classes
respectively. By default, the plugin will display the table heading and will not
recurse over nested configuration spec classes.
Here is an example for a configuration class `GlobalConfig` located in the
`myapp.config` module. We omit the table header and recurse over nested
configuration.
~~~ rst
.. envier:: myapp.config:GlobalConfig
:heading: false
:recursive: true
~~~
## Roadmap

@@ -116,0 +137,0 @@

@@ -92,2 +92,23 @@ <h1 align="center">Envier</h1>

## Sphinx Plugin
The library comes with a Sphinx plugin at `envier.sphinx` to allow generating
documentation from the configuration spec class directly. It exposes the
``envier`` directive that takes a mandatory argument, the configuration spec
class in the form `module:class`; additionally, the options `heading` and
`recursive` can be used to control whether to add heading and whether to
recursively get help information from nested configuration spec classes
respectively. By default, the plugin will display the table heading and will not
recurse over nested configuration spec classes.
Here is an example for a configuration class `GlobalConfig` located in the
`myapp.config` module. We omit the table header and recurse over nested
configuration.
~~~ rst
.. envier:: myapp.config:GlobalConfig
:heading: false
:recursive: true
~~~
## Roadmap

@@ -94,0 +115,0 @@

@@ -1,3 +0,1 @@

from ast import Sub
from riot import Venv

@@ -15,3 +13,6 @@ from riot import latest

Venv(
pkgs={"mypy": latest},
pkgs={
"mypy": latest,
"sphinx": latest,
},
pys=SUPPORTED_PYTHON_VERSIONS[2:],

@@ -38,3 +39,3 @@ ),

pkgs={"mypy": latest},
command="mypy {cmdargs}",
command="mypy --install-types --non-interactive {cmdargs}",
pys=["3"],

@@ -41,0 +42,0 @@ ),