Compare commits

..

No commits in common. "master" and "0.0.1" have entirely different histories.

25 changed files with 22 additions and 914 deletions

View File

@ -1,28 +0,0 @@
---
matrix:
include:
- IMAGE: 3.6-stretch
TOXENV: py36
- IMAGE: 3.7-stretch
TOXENV: py37
- IMAGE: 3.8-buster
TOXENV: py38
- IMAGE: 3.8-buster
TOXENV: lint
- IMAGE: 3.8-buster
TOXENV: sort
- IMAGE: 3.8-buster
TOXENV: format
- IMAGE: 3.8-buster
TOXENV: type
- IMAGE: 3.8-buster
TOXENV: docs
- IMAGE: 3.8-buster
TOXENV: metadata-release
pipeline:
build:
image: python:${IMAGE}
commands:
- pip install tox==3.14.6
- tox -e ${TOXENV}

View File

@ -1,2 +0,0 @@
# The path to our pass credentials store
export PASSWORD_STORE_DIR=$(pwd)/../infrastructure/credentials/password-store

View File

@ -1,42 +0,0 @@
Autonomic 0.0.5 (2020-04-13)
============================
Features
--------
- Add CoopHost decrypt command. (#5)
Autonomic 0.0.4 (2020-04-12)
============================
Features
--------
- Add CoopHost / CoopPass (WIP) commands. (#4)
- Further sanity checks. (#5)
Autonomic 0.0.3 (2020-04-11)
============================
Features
--------
- Add init command. (#2)
- Add Ansible actions command. (#3)
Autonomic 0.0.2 (2020-04-11)
============================
- Woops, missed a tag!
Autonomic 0.0.1 (2020-04-03)
============================
Project announcements
---------------------
- Initial project release! (#1)

View File

@ -20,10 +20,3 @@ autonomic
Command line utility belt for Autonomic
---------------------------------------
.. _documentation:
Documentation
-------------
Documentation coming soon TM.

View File

@ -1,4 +1,4 @@
"""Autonomic module."""
"""autonomic module."""
try:
import pkg_resources
@ -7,6 +7,6 @@ except ImportError:
try:
__version__ = pkg_resources.get_distribution("autonomic").version
__version__ = pkg_resources.get_distribution('autonomic').version
except Exception:
__version__ = "unknown"
__version__ = 'unknown'

View File

@ -2,12 +2,9 @@
import click
from autonomic.command import actions, coophost, cooppaas, init
@click.group()
@click.pass_context
def autonomic(ctx):
@click.command()
def main():
"""
\b
___ _ _
@ -16,14 +13,9 @@ def autonomic(ctx):
| _ | | | | __/ _ \| '_ \ / _ \| '_ ` _ \| |/ __|
| | | | |_| | || (_) | | | | (_) | | | | | | | (__
\_| |_/\__,_|\__\___/|_| |_|\___/|_| |_| |_|_|\___|
Hack the planet!
""" # noqa
pass
autonomic.add_command(init.init)
autonomic.add_command(actions.actions)
autonomic.add_command(coophost.coophost)
autonomic.add_command(cooppaas.cooppaas)
if __name__ == '__main__':
main()

View File

@ -1,38 +0,0 @@
"""Actions module."""
from os import environ
import click
from autonomic.config import ACTIONS_DIR, INFRA_DIR, PASS_STORE_DIR
from autonomic.infra import get_passwd, run_play
from autonomic.settings import get
from autonomic.utils import ensure_config_dir, git_status, question_ask
@click.command()
@click.pass_context
def actions(ctx):
"""Run an Ansible action."""
ensure_config_dir()
env = environ.copy()
env.update({"ANSIBLE_USER": get("username")})
env.update({"REMOTE_USER": get("username")})
env.update({"PASSWORD_STORE_DIR": PASS_STORE_DIR})
choices = ["addusers", "newhetzner", "rmhetzner", "newdokku", "pingall"]
action = question_ask("action", "Which Ansible action?", choices)
if any(action in choice for choice in ["newhetzner", "rmhetzner"]):
choices = ["prod", "test", "cicd"]
key = question_ask("key", "Which Hetzner API key?", choices)
path = "logins/hetzner/{}/api_key".format(key)
secret = get_passwd(path)
env["HCLOUD_TOKEN"] = secret
play = "{}/{}.yml".format(ACTIONS_DIR, action)
run_play(play, env=env)
git_status(INFRA_DIR)

View File

@ -1,136 +0,0 @@
"""CoopHost module."""
from os import chdir, mkdir
from os.path import basename, exists
from pathlib import Path
from socket import gethostname
import click
from autonomic.config import CONFIG_YAML, INFRA_DIR
from autonomic.logger import log
from autonomic.settings import add, get
from autonomic.utils import (
ensure_config_dir,
ensure_deploy_d_dir,
exit,
input_ask,
pass_ask,
question_ask,
run,
yaml_dump,
yaml_load,
)
hostname = gethostname()
@click.command()
@click.pass_context
def coophost(ctx):
"""Manage CoopHost resources."""
ensure_config_dir()
ensure_deploy_d_dir()
app_dir = Path(".").absolute()
app = basename(app_dir)
log.info("Auto-detected the {} application".format(app))
choices = ["encrypt", "decrypt"]
operation = question_ask("operation", "Which operation?", choices)
if operation == "encrypt":
encrypt(app, app_dir)
elif operation == "decrypt":
decrypt(app, app_dir)
def get_vault_pass(app):
"""Retrieve or set the app vault password."""
app_settings = get(app)
if app_settings is not None and "vault-password" in app_settings:
log.info("Using app vault password stored in {}".format(CONFIG_YAML))
return app_settings["vault-password"]
log.info("No app vault password configured")
vault_password = pass_ask("Vault password?")
log.info("App vault password stored in {}".format(CONFIG_YAML))
add({app: {"vault-password": vault_password}})
return vault_password
def decrypt(app, app_dir):
"""Decrypt a secret."""
vault_password = get_vault_pass(app)
name = input_ask("Which variable do you want to decrypt?")
vault_path = (Path(".") / "deploy.d" / "vault").absolute()
var_path = (vault_path / "{}.yml".format(name)).absolute()
if not exists(var_path):
exit("{}.yml is missing?".format(name))
cmd = [
".venv/bin/ansible",
hostname,
"--inventory",
"{},".format(hostname),
"-m",
"debug",
"-a",
"var='{}'".format(name),
"-e @{}".format(var_path),
"--ask-vault-pass",
"-e",
"ansible_user={}".format(get("username")),
]
decrypted = run(
cmd,
cwd=INFRA_DIR,
output=True,
pexpect=True,
pexpected={"(?i)vault password:": vault_password},
)
log.info(decrypted)
def encrypt(app, app_dir):
"""Encrypt a secret for a CoopHost package."""
vault_password = get_vault_pass(app)
name = input_ask("Which variable do you want to encrypt?")
value = pass_ask("Variable value to encrypt?")
cmd = [".venv/bin/ansible-vault", "encrypt_string", "--name", name, value]
encrypted = run(
cmd,
cwd=INFRA_DIR,
pexpect=True,
pexpected={
"(?i)new vault password:": vault_password,
"(?i)confirm new vault password:": vault_password,
},
)
encrypted = (
encrypted.strip()
.replace("\r", "")
.replace("\nEncryption successful", "")
)
chdir(app_dir)
log.info("Changed directory back to {}".format(app_dir))
vault_path = (Path(".") / "deploy.d" / "vault").absolute()
if not exists(vault_path):
log.info("Creating {}".format(vault_path))
mkdir(vault_path)
var_path = (vault_path / "{}.yml".format(name)).absolute()
with open(var_path, "w"):
loaded = yaml_load(encrypted, text=True)
yaml_dump(var_path, loaded)
log.success("Encrypted and saved {} in {}".format(name, var_path))

View File

@ -1,12 +0,0 @@
"""CoopPaas module."""
import click
from autonomic.logger import log
@click.command()
@click.pass_context
def cooppaas(ctx):
"""Manage CoopPaas resources."""
log.info("Still experimenting...")

View File

@ -1,89 +0,0 @@
"""Initialise the toolbelt."""
from os import mkdir
from os.path import exists
from subprocess import STDOUT
import click
from emoji import emojize
from autonomic.config import CONFIG_DIR, CONFIG_YAML, INFRA_DIR, INFRA_REPO
from autonomic.infra import members
from autonomic.logger import log
from autonomic.settings import add
from autonomic.utils import is_proc, question_ask, run
@click.command()
@click.pass_context
def init(ctx):
"""Initialise the toolbelt."""
create_configuration()
clone_infrastructure_repo()
store_username()
install_dependencies()
ssh_sanity_check()
msg = "Hack the planet! :earth_africa:"
log.success(emojize(msg, use_aliases=True))
def create_configuration():
"""Create toolbelt config directory."""
if not exists(CONFIG_DIR):
mkdir(CONFIG_DIR)
if not exists(CONFIG_YAML):
with open(CONFIG_YAML, "w") as handle:
handle.write("---")
def clone_infrastructure_repo():
"""Clone or update the infrastructure repository."""
if not exists(INFRA_DIR):
cmd = ["git", "clone", INFRA_REPO, INFRA_DIR]
run(cmd, stderr=STDOUT)
else:
cmd = ["git", "pull", "origin", "master"]
run(cmd, cwd=INFRA_DIR, stderr=STDOUT)
def store_username():
"""Store Autonomic username in the settings."""
usernames = members(flatten="username")
username = question_ask(
"username", "What is you Autonomic username?", usernames,
)
add({"username": username})
msg = "Welcome comrade {} :kissing:".format(username)
log.success(emojize(msg, use_aliases=True))
def install_dependencies():
"""Install infrastructure dependencies."""
cmd = ["/usr/bin/python3", "-m", "venv", ".venv"]
run(cmd, cwd=INFRA_DIR, stderr=STDOUT)
cmd = [".venv/bin/pip", "install", "-r", "requirements.txt"]
run(cmd, stderr=STDOUT)
cmd = [
".venv/bin/ansible-galaxy",
"install",
"-r",
"requirements.yml",
"--force",
]
run(cmd, stderr=STDOUT)
def ssh_sanity_check():
"""Try to recommend some SSH tips."""
if not is_proc("ssh-agent"):
msg = "warning: ssh-agent is not running :sob:"
log.warning(emojize(msg, use_aliases=True))
else:
msg = "ssh-agent is running :rocket:"
log.success(emojize(msg, use_aliases=True))

View File

@ -1,28 +0,0 @@
"""Tool configuration."""
from os.path import expanduser
HOME_DIR = expanduser("~")
# configuration directory for the toolbelt
CONFIG_DIR = "{}/.autonomic".format(HOME_DIR)
# SSH url for our infrastructure repository
INFRA_REPO = (
"ssh://git@git.autonomic.zone:222/autonomic-cooperative/infrastructure.git"
)
# infrastructure directory
INFRA_DIR = "{}/infrastructure".format(CONFIG_DIR)
# list of autonomic members yaml file
MEMBERS_YAML = "{}/resources/members.yml".format(INFRA_DIR)
# directory where we store our ansible action plays
ACTIONS_DIR = "{}/actions".format(INFRA_DIR)
# toolbelt configuration file
CONFIG_YAML = "{}/autonomic.yml".format(CONFIG_DIR)
# password store directory
PASS_STORE_DIR = "{}/credentials/password-store".format(INFRA_DIR)

View File

@ -1,43 +0,0 @@
"""Infrastructure module.
All functionality that requires interacting with the infrastructure repository
should live in here. This module relies on the file structure and conventions
of the infrastructure repository directory layout. That can change without the
code knowing, so the code must act more defensively than normal. This is an
experiment.
"""
from os import environ
from autonomic.config import INFRA_DIR, MEMBERS_YAML, PASS_STORE_DIR
from autonomic.utils import run, yaml_load
def members(flatten=None):
"""The list of Autonomic members."""
members = yaml_load(MEMBERS_YAML)
if members is None:
return
if flatten:
return [info[flatten] for info in members["autonomic_members"]]
return members
def run_play(play, env=None):
"""Run an Ansible playbook."""
cmd = [".venv/bin/ansible-playbook", play]
run(cmd, cwd=INFRA_DIR, interactive=True, env=env)
def get_passwd(path):
"""Lookup a password from the password store."""
env = environ.copy()
env.update({"PASSWORD_STORE_DIR": PASS_STORE_DIR})
cmd = ["pass", "show", path]
output = run(cmd, cwd=INFRA_DIR, env=env)
return output.strip()

View File

@ -1,150 +0,0 @@
"""Logging Module.
This module is largely borrowed from Ansible Molecule and that is why it is
largely uncommented since we didn't write it and luckily, didn't have to! This
code just works TM. The same upstream license applies.
"""
import logging
import os
import sys
import colorama
def should_do_markup():
return sys.stdout.isatty() and os.environ.get("TERM") != "dumb"
SUCCESS = 100
OUT = 101
class LogFilter(object):
def __init__(self, level):
self.__level = level
def filter(self, logRecord):
return logRecord.levelno <= self.__level
class CustomLogger(logging.getLoggerClass()): # type: ignore
def __init__(self, name, level=logging.NOTSET):
super(logging.getLoggerClass(), self).__init__(name, level)
logging.addLevelName(SUCCESS, "SUCCESS")
logging.addLevelName(OUT, "OUT")
def success(self, msg, *args, **kwargs):
if self.isEnabledFor(SUCCESS):
self._log(SUCCESS, msg, args, **kwargs)
def out(self, msg, *args, **kwargs):
if self.isEnabledFor(OUT):
self._log(OUT, msg, args, **kwargs)
class TrailingNewlineFormatter(logging.Formatter):
def format(self, record):
if record.msg:
record.msg = record.msg.rstrip()
return super(TrailingNewlineFormatter, self).format(record)
def get_logger(name=None):
logging.setLoggerClass(CustomLogger)
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
logger.addHandler(_get_info_handler())
logger.addHandler(_get_out_handler())
logger.addHandler(_get_warn_handler())
logger.addHandler(_get_error_handler())
logger.addHandler(_get_critical_handler())
logger.addHandler(_get_success_handler())
logger.propagate = False
return logger
def _get_info_handler():
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.INFO)
handler.addFilter(LogFilter(logging.INFO))
handler.setFormatter(
TrailingNewlineFormatter("--> {}".format(cyan_text("%(message)s")))
)
return handler
def _get_out_handler():
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(OUT)
handler.addFilter(LogFilter(OUT))
handler.setFormatter(TrailingNewlineFormatter(" %(message)s"))
return handler
def _get_warn_handler():
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.WARN)
handler.addFilter(LogFilter(logging.WARN))
handler.setFormatter(TrailingNewlineFormatter(yellow_text("%(message)s")))
return handler
def _get_error_handler():
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(logging.ERROR)
handler.addFilter(LogFilter(logging.ERROR))
handler.setFormatter(TrailingNewlineFormatter(red_text("%(message)s")))
return handler
def _get_critical_handler():
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(logging.CRITICAL)
handler.addFilter(LogFilter(logging.CRITICAL))
handler.setFormatter(
TrailingNewlineFormatter(red_text("ERROR: %(message)s"))
)
return handler
def _get_success_handler():
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(SUCCESS)
handler.addFilter(LogFilter(SUCCESS))
handler.setFormatter(TrailingNewlineFormatter(green_text("%(message)s")))
return handler
def red_text(msg):
return color_text(colorama.Fore.RED, msg)
def yellow_text(msg):
return color_text(colorama.Fore.YELLOW, msg)
def green_text(msg):
return color_text(colorama.Fore.GREEN, msg)
def cyan_text(msg):
return color_text(colorama.Fore.CYAN, msg)
def color_text(color, msg):
if should_do_markup():
return "{}{}{}".format(color, msg, colorama.Style.RESET_ALL)
return msg
log = get_logger(__name__)

View File

@ -1,49 +0,0 @@
"""Toolbelt settings module.
All code related to working with the ~/.autonomic/settings.yml file. This file
is useful for storing information for future lookup when dealing with
repetitive cooperative tasks.
"""
from typing import Dict
from autonomic.config import CONFIG_YAML
from autonomic.utils import yaml_dump, yaml_load
def add(yaml: Dict[str, str]):
"""Add YAML to the settings file."""
from autonomic.command.init import create_configuration
create_configuration()
settings = yaml_load(CONFIG_YAML)
if settings is None:
settings = {}
for key in yaml:
settings[key] = yaml[key]
yaml_dump(CONFIG_YAML, settings)
def remove(key):
"""Remove key from the settings file."""
settings = yaml_load(CONFIG_YAML)
if settings is None:
return
settings.pop(key, None)
yaml_dump(CONFIG_YAML, settings)
def get(key):
"""Retrieve settings key."""
settings = yaml_load(CONFIG_YAML)
try:
return settings[key]
except Exception:
return None

View File

@ -1,174 +0,0 @@
"""Utility functions.
While mostly a vague concept and sometimes a bad idea, the utilities module is
somewhere where reusable system related, data processing and other general
functions can live. This codebase seems to have a few of those, so it seems
like a necessary module. Let us be critical of our requirements when adding
things here.
"""
from os import chdir
from os.path import exists
from pathlib import Path
from subprocess import call, check_output
from sys import exit as sys_exit
from pexpect import spawn
from psutil import process_iter
from PyInquirer import prompt
from autonomic.config import CONFIG_DIR, CONFIG_YAML, INFRA_DIR
from autonomic.logger import log
from autonomic.yaml import yaml
def run(
cmd, cwd=None, interactive=False, pexpect=False, pexpected=None, **kwargs
):
"""Run a system command.
Please note, all **kwargs will be passed into the check_output command so
that system call customisation can happen. Please name your keyword
arguments (like `cwd`) if you intend that they are used for other logic.
"""
try:
if cwd:
chdir(cwd)
log.info("Changed directory to {}".format(cwd))
log.info("Running {}".format(" ".join(cmd)))
if interactive:
return call(cmd, **kwargs)
if pexpect:
child = spawn(" ".join(cmd))
for expected, response in pexpected.items():
try:
child.expect(expected, timeout=5)
child.sendline(response)
except Exception as exception:
msg = "Pexpecting failed, saw {}".format(str(exception))
exit(msg)
return child.read().decode("utf-8")
output = check_output(cmd, **kwargs)
return output.decode("utf-8")
except Exception as exception:
msg = "{} failed! Saw {}".format(" ".join(cmd), str(exception))
exit(msg)
def exit(msg, code=1):
"""Exit and log appropriate level."""
if code != 0:
log.critical(msg)
else:
log.info(msg)
sys_exit(code)
def question_ask(name, message, choices):
"""Ask a question."""
question = [
{
"type": "list",
"name": name,
"message": message,
"choices": choices,
"filter": lambda answer: answer.lower(),
}
]
return answer(question, name)
def pass_ask(message):
"""Ask for a password."""
question = [{"type": "password", "message": message, "name": "password"}]
return answer(question, "password")
def input_ask(message):
"""Ask for input."""
question = [{"type": "input", "message": message, "name": "input"}]
return answer(question, "input")
def answer(question, key):
"""Retrieve an answer from a prompt"""
result = prompt(question)
if not result:
exit("Prompt cancelled")
try:
return result[key]
except KeyError:
exit("{} key is missing".format(key))
def yaml_load(target, text=False):
"""Load a YAML file or text."""
if text is True:
try:
return yaml.load(target)
except Exception as exception:
log.error(str(exception))
try:
with open(target, "r") as handle:
return yaml.load(handle.read())
except Exception as exception:
log.error(str(exception))
def yaml_dump(fpath, data):
"""Dump a YAML file."""
try:
with open(fpath, "w") as handle:
yaml.dump(data, handle)
except Exception as exception:
log.error(str(exception))
def is_proc(name):
"""Determine if a process is running or not."""
for process in process_iter():
try:
if name.lower() in process.name().lower():
return True
except Exception:
pass
return False
def git_status(fpath):
"""Check if Git reports changes to be committed."""
cmd = ["git", "status", "--porcelain"]
output = run(cmd, cwd=fpath)
if output:
msg = "warning: git reports uncommitted changes in {}".format(fpath)
log.warning(msg)
log.warning(output)
else:
msg = "No unstaged changes found in {}".format(INFRA_DIR)
log.info(msg)
def ensure_config_dir():
"""Ensure configuration directory is in place."""
if not exists(CONFIG_DIR):
msg = "{} is missing, did you run 'autonomic init'?".format(CONFIG_YAML)
exit(msg)
def ensure_deploy_d_dir():
"""Ensure deploy.d directory is in place."""
deploy_d_dir = (Path(".") / "deploy.d").absolute()
if not exists(deploy_d_dir):
msg = "No deploy.d folder found, are you in the right place?"
exit(msg)

View File

@ -1,13 +0,0 @@
"""YAML initialisation module.
Since we would like a consistent approach to how we load, manipulate and dump
YAML, we centralise the approach into a single module to avoid duplicating all
the customisation code across the code base.
"""
from ruamel.yaml import YAML
yaml = YAML()
# ensure that "---" is always at the top of the YAML file
yaml.explicit_start = True # type: ignore

View File

@ -1,8 +1,8 @@
author = "decentral1se"
copyright = "2019, decentral1se"
html_static_path = ["_static"]
html_theme = "sphinx_rtd_theme"
master_doc = "index"
project = "autonomic utility belt"
templates_path = ["_templates"]
extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"]
author = 'decentral1se'
copyright = '2019, decentral1se'
html_static_path = ['_static']
html_theme = 'sphinx_rtd_theme'
master_doc = 'index'
project = 'autonomic'
templates_path = ['_templates']
extensions = ['sphinx.ext.autodoc', 'sphinx_autodoc_typehints']

View File

@ -20,9 +20,11 @@ black = "^19.10b0"
isort = "^4.3.21"
flake8 = "^3.7.9"
[tool.black]
line-length = 80
target-version = ["py38"]
skip-string-normalization = true
include = '\.pyi?$'
[tool.towncrier]

View File

@ -6,7 +6,7 @@ max-line-length = 80
[isort]
known_first_party = autonomic
known_third_party = pytest, psutil
known_third_party = pytest
line_length = 80
multi_line_output = 3
skip = .venv, .tox
@ -43,19 +43,13 @@ packages = find:
zip_safe = False
install_requires =
click >= 7.1.1, <= 8.0
colorama >= 0.4.3, <= 0.5
emoji >= 0.5.4, <= 0.6
pexpect >= 4.8.0, <= 4.9
psutil >= 5.7.0, <= 6.0
pyinquirer >= 1.0.3, <= 1.1
ruamel.yaml >= 0.16.10, <= 0.17
[options.packages.find]
where = .
[options.entry_points]
console_scripts =
autonomic = autonomic.__main__:autonomic
autonomic = autonomic.__main__:main
[build_sphinx]
all_files = 1

View File

@ -1,20 +0,0 @@
import pytest
from autonomic.settings import add, get, remove
@pytest.fixture
def config_yml():
from autonomic.command.init import create_configuration
create_configuration()
def test_add_get_remove(config_yml):
add({"foo": "bar"})
assert get("doesnt-exist") is None
assert get("foo") == "bar"
remove("foo")
assert get("foo") is None

View File

@ -1,44 +0,0 @@
from subprocess import STDOUT
from autonomic.utils import is_proc, run, yaml_dump, yaml_load
def test_run_kwargs():
assert run(["whoami"], stderr=STDOUT) is not None
def test_run_cwd(tmp_path):
directory = tmp_path / "test"
directory.mkdir()
testfile = directory / "testfile.txt"
testfile.write_text("hello, world")
output = run(["ls"], cwd=directory.absolute())
assert "testfile.txt" in output
def test_yaml_load(tmp_path):
directory = tmp_path / "test"
directory.mkdir()
testfile = directory / "testfile.yml"
testfile.write_text("---\nusername: foobar")
loaded = yaml_load(testfile.absolute())
assert loaded["username"] == "foobar"
def test_yaml_dump(tmp_path):
directory = tmp_path / "test"
directory.mkdir()
testfile = directory / "testfile.yml"
yaml_dump(testfile.absolute(), {"username": "foobar"})
loaded = yaml_load(testfile.absolute())
assert loaded["username"] == "foobar"
def test_is_proc():
assert is_proc("pytest") is True

View File

@ -2,9 +2,9 @@
def test_version_fails_gracefully(mocker):
target = "pkg_resources.get_distribution"
target = 'pkg_resources.get_distribution'
mocker.patch(target, side_effect=Exception())
from autonomic.__init__ import __version__
assert __version__ == "unknown"
assert __version__ == 'unknown'

View File

@ -1,5 +0,0 @@
from autonomic.yaml import yaml
def test_yaml_config():
assert yaml.explicit_start is True

View File

@ -6,6 +6,7 @@ envlist =
format
type
docs
changelog
metadata-release
skip_missing_interpreters = True
isolated_build = True
@ -49,7 +50,6 @@ description = build the documentation
skipdist = True
deps =
sphinx
sphinx-autodoc-typehints
sphinx_rtd_theme
commands = python -m setup build_sphinx