Initialise bootstrapping of repository

Missing tests but most of the copy/pasta is done.
This commit is contained in:
Luke Murphy 2019-09-27 22:00:44 +02:00
commit e96a71866d
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
15 changed files with 1006 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Luke Murphy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

73
README.rst Normal file
View File

@ -0,0 +1,73 @@
*****************************
Molecule Hetzner Cloud Plugin
*****************************
.. image:: https://badge.fury.io/py/molecule-hetznercloud.svg
:target: https://badge.fury.io/py/molecule-hetznercloud
:alt: PyPI Package
.. image:: https://img.shields.io/travis/com/pycontribs/molecule-hetznercloud/master.svg?label=Linux%20builds%20%40%20Travis%20CI
:target: https://travis-ci.com/pycontribs/molecule-hetznercloud
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/python/black
:alt: Python Black Code Style
.. image:: https://img.shields.io/badge/Code%20of%20Conduct-Ansible-silver.svg
:target: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
:alt: Ansible Code of Conduct
.. image:: https://img.shields.io/badge/Mailing%20lists-Ansible-orange.svg
:target: https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information
:alt: Ansible mailing lists
.. image:: https://img.shields.io/badge/license-MIT-brightgreen.svg
:target: LICENSE
:alt: Repository License
Molecule Hetzner Cloud driver plugin.
Documentation
=============
> https://molecule.readthedocs.io
.. _get-involved:
Get Involved
============
* Join us in the ``#ansible-molecule`` channel on `Freenode`_.
* Join the discussion in `molecule-users Forum`_.
* Join the community working group by checking the `wiki`_.
* Want to know about releases, subscribe to `ansible-announce list`_.
* For the full list of Ansible email Lists, IRC channels see the
`communication page`_.
.. _`Freenode`: https://freenode.net
.. _`molecule-users Forum`: https://groups.google.com/forum/#!forum/molecule-users
.. _`wiki`: https://github.com/ansible/community/wiki/Molecule
.. _`ansible-announce list`: https://groups.google.com/group/ansible-announce
.. _`communication page`: https://docs.ansible.com/ansible/latest/community/communication.html
.. _authors:
Authors
=======
Molecule Hetzner Cloud Plugin was created by Luke Murphy based on code from Molecule.
.. _license:
License
=======
The `MIT`_ License.
.. _`MIT`: https://github.com/ansible/molecule/blob/master/LICENSE
The logo is licensed under the `Creative Commons NoDerivatives 4.0 License`_.
If you have some other use in mind, contact us.
.. _`Creative Commons NoDerivatives 4.0 License`: https://creativecommons.org/licenses/by-nd/4.0/

View File

View File

@ -0,0 +1,5 @@
{
"molecule_directory": "molecule",
"role_name": "OVERRIDDEN",
"scenario_name": "OVERRIDDEN"
}

View File

@ -0,0 +1,23 @@
***************************************
Hetzner Cloud driver installation guide
***************************************
Requirements
============
* Ansible >= 2.8
* ``HCLOUD_TOKEN`` exposed in your environment
Install
=======
Please refer to the `Virtual environment`_ documentation for installation best
practices. If not using a virtual environment, please consider passing the
widely recommended `'--user' flag`_ when invoking ``pip``.
.. _Virtual environment: https://virtualenv.pypa.io/en/latest/
.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site
.. code-block:: bash
$ pip install 'molecule[hetznercloud]'

View File

@ -0,0 +1,90 @@
---
{% raw -%}
- name: Create
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
vars:
ssh_port: 22
ssh_user: root
ssh_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
tasks:
- name: Create SSH key
user:
name: "{{ lookup('env', 'USER') }}"
generate_ssh_key: true
ssh_key_file: "{{ ssh_path }}"
force: true
register: generated_ssh_key
- name: Register the SSH key name
set_fact:
ssh_key_name: "molecule-generated-{{ 12345 | random | to_uuid }}"
- name: Register SSH key for test instance(s)
hcloud_ssh_key:
name: "{{ ssh_key_name }}"
public_key: "{{ generated_ssh_key.ssh_public_key }}"
state: present
- name: Create molecule instance(s)
hcloud_server:
name: "{{ item.name }}"
server_type: "{{ item.server_type }}"
ssh_keys:
- "{{ ssh_key_name }}"
volumes: "{{ item.volumes | default(omit) }}"
image: "{{ item.image }}"
location: "{{ item.location | default(omit) }}"
datacenter: "{{ item.datacenter | default(omit) }}"
user_data: "{{ item.user_data | default(omit) }}"
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
state: present
register: server
with_items: "{{ molecule_yml.platforms }}"
async: 7200
poll: 0
- name: Wait for instance(s) creation to complete
async_status:
jid: "{{ item.ansible_job_id }}"
register: hetzner_jobs
until: hetzner_jobs.finished
retries: 300
with_items: "{{ server.results }}"
# Mandatory configuration for Molecule to function.
- name: Populate instance config dict
set_fact:
instance_conf_dict: {
'instance': "{{ item.hcloud_server.name }}",
'ssh_key_name': "{{ ssh_key_name }}",
'address': "{{ item.hcloud_server.ipv4_address }}",
'user': "{{ ssh_user }}",
'port': "{{ ssh_port }}",
'identity_file': "{{ ssh_path }}", }
with_items: "{{ hetzner_jobs.results }}"
register: instance_config_dict
when: server.changed | bool
- name: Convert instance config dict to a list
set_fact:
instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"
when: server.changed | bool
- name: Dump instance config
copy:
content: "{{ instance_conf | to_json | from_json | molecule_to_yaml | molecule_header }}"
dest: "{{ molecule_instance_config }}"
when: server.changed | bool
- name: Wait for SSH
wait_for:
port: "{{ ssh_port }}"
host: "{{ item.address }}"
search_regex: SSH
delay: 10
with_items: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"
{%- endraw %}

View File

@ -0,0 +1,59 @@
---
{% raw -%}
- name: Destroy
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
tasks:
- name: Populate the instance config
block:
- name: Populate instance config from file
set_fact:
instance_conf: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"
skip_instances: false
rescue:
- name: Populate instance config when file missing
set_fact:
instance_conf: {}
skip_instances: true
- name: Destroy molecule instance(s)
hcloud_server:
name: "{{ item.instance }}"
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
state: absent
register: server
with_items: "{{ instance_conf }}"
when: not skip_instances
async: 7200
poll: 0
- name: Wait for instance(s) deletion to complete
async_status:
jid: "{{ item.ansible_job_id }}"
register: hetzner_jobs
until: hetzner_jobs.finished
retries: 300
with_items: "{{ server.results }}"
- name: Remove registered SSH key
hcloud_ssh_key:
name: "{{ instance_conf[0].ssh_key_name }}"
state: absent
when:
- not skip_instances
- instance_conf # must contain at least one instance
# Mandatory configuration for Molecule to function.
- name: Populate instance config
set_fact:
instance_conf: {}
- name: Dump instance config
copy:
content: "{{ instance_conf | molecule_to_yaml | molecule_header }}"
dest: "{{ molecule_instance_config }}"
when: server.changed | bool
{%- endraw %}

View File

@ -0,0 +1,7 @@
---
- name: Converge
hosts: all
tasks:
- name: "Include {{ cookiecutter.role_name }}"
include_role:
name: "{{ cookiecutter.role_name }}"

View File

@ -0,0 +1,11 @@
---
{% raw -%}
- name: Prepare
hosts: all
gather_facts: false
tasks:
- name: Install python for Ansible
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-zipstream)
become: true
changed_when: false
{%- endraw %}

View File

@ -0,0 +1,174 @@
# Copyright (c) 2019 Red Hat, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
import os
from molecule import logger, util
from molecule.api import Driver
from molecule.util import lru_cache
from molecule.util import sysexit_with_message
log = logger.get_logger(__name__)
class HetznerCloud(Driver):
"""
The class responsible for managing `Hetzner Cloud`_ instances.
`Hetzner Cloud`_ is **not** the default driver used in Molecule.
Molecule leverages Ansible's `hcloud_server module`_, by mapping variables
from ``molecule.yml`` into ``create.yml`` and ``destroy.yml``.
.. important::
The ``hcloud_server`` module is only available in Ansible >= 2.8.
.. _`hcloud_server module`: https://docs.ansible.com/ansible/devel/modules/hcloud_server_module.html#hcloud-server-module
.. code-block:: yaml
driver:
name: hetznercloud
platforms:
- name: instance
server_type: cx11
image: debian-9
.. code-block:: bash
$ pip install 'molecule[hetznercloud]'
Change the options passed to the ssh client.
.. code-block:: yaml
driver:
name: hetznercloud
ssh_connection_options:
- '-o ControlPath=~/.ansible/cp/%r@%h-%p'
.. important::
The Hetzner Cloud driver implementation uses the Parmiko transport
provided by Ansible to avoid issues of connection hanging and
indefinite polling as experienced with the default OpenSSh based
transport.
.. important::
Molecule does not merge lists, when overriding the developer must
provide all options.
Provide the files Molecule will preserve upon each subcommand execution.
.. code-block:: yaml
driver:
name: hetznercloud
safe_files:
- foo
""" # noqa
def __init__(self, config=None):
super(HetznerCloud, self).__init__(config)
self._name = 'hetznercloud'
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@property
def login_cmd_template(self):
connection_options = ' '.join(self.ssh_connection_options)
return (
'ssh {{address}} '
'-l {{user}} '
'-p {{port}} '
'-i {{identity_file}} '
'{}'
).format(connection_options)
@property
def default_safe_files(self):
return [self.instance_config]
@property
def default_ssh_connection_options(self):
return self._get_ssh_connection_options()
def login_options(self, instance_name):
d = {'instance': instance_name}
return util.merge_dicts(d, self._get_instance_config(instance_name))
def ansible_connection_options(self, instance_name):
try:
d = self._get_instance_config(instance_name)
return {
'ansible_user': d['user'],
'ansible_host': d['address'],
'ansible_port': d['port'],
'ansible_private_key_file': d['identity_file'],
'connection': 'ssh',
'ansible_ssh_common_args': ' '.join(self.ssh_connection_options),
}
except StopIteration:
return {}
except IOError:
# Instance has yet to be provisioned , therefore the
# instance_config is not on disk.
return {}
def _get_instance_config(self, instance_name):
instance_config_dict = util.safe_load_file(self._config.driver.instance_config)
return next(
item for item in instance_config_dict if item['instance'] == instance_name
)
@lru_cache()
def sanity_checks(self):
"""Hetzner Cloud driver sanity checks."""
log.info("Sanity checks: '{}'".format(self._name))
try:
import hcloud # noqa
except ImportError:
msg = (
'Missing Hetzner Cloud driver dependency. Please '
"install via 'molecule[hetznercloud]' or refer to "
'your INSTALL.rst driver documentation file'
)
sysexit_with_message(msg)
if 'HCLOUD_TOKEN' not in os.environ:
msg = (
'Missing Hetzner Cloud API token. Please expose '
'the HCLOUD_TOKEN environment variable with your '
'account API token value'
)
sysexit_with_message(msg)

11
pyproject.toml Normal file
View File

@ -0,0 +1,11 @@
[build-system]
requires = [
"setuptools >= 41.0.0",
"setuptools_scm >= 1.15.0",
"setuptools_scm_git_archive >= 1.0",
"wheel",
]
build-backend = "setuptools.build_meta"
[tool.black]
skip-string-normalization = true

12
pytest.ini Normal file
View File

@ -0,0 +1,12 @@
[pytest]
addopts = -v -rxXs --doctest-modules --durations 10 --cov=molecule --cov-report term-missing:skip-covered --cov-report xml -m "not molecule"
doctest_optionflags = ALLOW_UNICODE ELLIPSIS
junit_suite_name = molecule_test_suite
norecursedirs = dist doc build .tox .eggs
filterwarnings =
# remove once https://github.com/cookiecutter/cookiecutter/pull/1127 is released
ignore::DeprecationWarning:cookiecutter
# remove once https://github.com/pytest-dev/pytest-cov/issues/327 is released
ignore::pytest.PytestWarning:pytest_cov
# remove once https://bitbucket.org/astanin/python-tabulate/issues/174/invalid-escape-sequence
ignore::DeprecationWarning:tabulate

88
setup.cfg Normal file
View File

@ -0,0 +1,88 @@
[aliases]
dists = clean --all sdist bdist_wheel
[bdist_wheel]
universal = 1
[metadata]
name = molecule-hetznercloud
url = https://github.com/pycontribs/molecule-hetznercloud
project_urls =
Bug Tracker = https://github.com/pycontribs/molecule-hetznercloud/issues
Release Management = https://github.com/pycontribs/molecule-hetznercloud/projects
CI: Travis = https://travis-ci.com/pycontribs/molecule-hetznercloud
Source Code = https://github.com/pycontribs/molecule-hetznercloud
description = Hetznercloud Molecule Plugin :: run molecule tests with hetznercloud as verifier
long_description = file: README.rst
long_description_content_type = text/x-rst
author = Luke Murphy
author_email = lukewm@riseup.net
maintainer = Luke Murphy
maintainer_email = lukewm@riseup.net
license = MIT
license_file = LICENSE
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
Intended Audience :: Developers
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: MIT License
Natural Language :: English
Operating System :: OS Independent
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Topic :: System :: Systems Administration
Topic :: Utilities
keywords =
ansible
roles
testing
molecule
plugin
hetznercloud
verifier
[options]
use_scm_version = True
python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*
packages = find:
include_package_data = True
zip_safe = False
setup_requires =
setuptools_scm >= 1.15.0
setuptools_scm_git_archive >= 1.0
install_requires =
molecule >= 3.0a3
pyyaml >= 5.1, < 6
backports.shutil_which ; python_version<"3.3"
[options.extras_require]
test =
flake8>=3.6.0, < 4
mock>=3.0.5, < 4
pytest>=4.6.3, < 5
pytest-cov>=2.7.1, < 3
pytest-helpers-namespace>=2019.1.8, < 2020
pytest-mock>=1.10.4, < 2
pytest-verbose-parametrize>=1.7.0, < 2
pytest-xdist>=1.29.0, < 2
hcloud>=1.2.1
[options.entry_points]
molecule.verifier =
hetznercloud = molecule_hetznercloud.hetznercloud:hetznercloud
[options.packages.find]
where = .

330
setup.py Normal file
View File

@ -0,0 +1,330 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import setuptools
HAS_DIST_INFO_CMD = False
try:
import setuptools.command.dist_info
HAS_DIST_INFO_CMD = True
except ImportError:
"""Setuptools version is too old."""
ALL_STRING_TYPES = tuple(map(type, ("", b"", u"")))
MIN_NATIVE_SETUPTOOLS_VERSION = 34, 4, 0
"""Minimal setuptools having good read_configuration implementation."""
RUNTIME_SETUPTOOLS_VERSION = tuple(map(int, setuptools.__version__.split(".")))
"""Setuptools imported now."""
READ_CONFIG_SHIM_NEEDED = RUNTIME_SETUPTOOLS_VERSION < MIN_NATIVE_SETUPTOOLS_VERSION
def str_if_nested_or_str(s):
"""Turn input into a native string if possible."""
if isinstance(s, ALL_STRING_TYPES):
return str(s)
if isinstance(s, (list, tuple)):
return type(s)(map(str_if_nested_or_str, s))
if isinstance(s, (dict,)):
return stringify_dict_contents(s)
return s
def stringify_dict_contents(dct):
"""Turn dict keys and values into native strings."""
return {str_if_nested_or_str(k): str_if_nested_or_str(v) for k, v in dct.items()}
if not READ_CONFIG_SHIM_NEEDED:
from setuptools.config import read_configuration, ConfigOptionsHandler
import setuptools.config
import setuptools.dist
# Set default value for 'use_scm_version'
setattr(setuptools.dist.Distribution, "use_scm_version", False)
# Attach bool parser to 'use_scm_version' option
class ShimConfigOptionsHandler(ConfigOptionsHandler):
"""Extension class for ConfigOptionsHandler."""
@property
def parsers(self):
"""Return an option mapping with default data type parsers."""
_orig_parsers = super(ShimConfigOptionsHandler, self).parsers
return dict(use_scm_version=self._parse_bool, **_orig_parsers)
def parse_section_packages__find(self, section_options):
find_kwargs = super(
ShimConfigOptionsHandler, self
).parse_section_packages__find(section_options)
return stringify_dict_contents(find_kwargs)
setuptools.config.ConfigOptionsHandler = ShimConfigOptionsHandler
else:
"""This is a shim for setuptools<required."""
import functools
import io
import json
import sys
import warnings
try:
import setuptools.config
def filter_out_unknown_section(i):
def chi(self, *args, **kwargs):
i(self, *args, **kwargs)
self.sections = {
s: v for s, v in self.sections.items() if s != "packages.find"
}
return chi
setuptools.config.ConfigHandler.__init__ = filter_out_unknown_section(
setuptools.config.ConfigHandler.__init__
)
except ImportError:
pass
def ignore_unknown_options(s):
@functools.wraps(s)
def sw(**attrs):
try:
ignore_warning_regex = (
r"Unknown distribution option: "
r"'(license_file|project_urls|python_requires)'"
)
warnings.filterwarnings(
"ignore",
message=ignore_warning_regex,
category=UserWarning,
module="distutils.dist",
)
return s(**attrs)
finally:
warnings.resetwarnings()
return sw
def parse_predicates(python_requires):
import itertools
import operator
sorted_operators_map = tuple(
sorted(
{
">": operator.gt,
"<": operator.lt,
">=": operator.ge,
"<=": operator.le,
"==": operator.eq,
"!=": operator.ne,
"": operator.eq,
}.items(),
key=lambda i: len(i[0]),
reverse=True,
)
)
def is_decimal(s):
return type(u"")(s).isdecimal()
conditions = map(str.strip, python_requires.split(","))
for c in conditions:
for op_sign, op_func in sorted_operators_map:
if not c.startswith(op_sign):
continue
raw_ver = itertools.takewhile(
is_decimal, c[len(op_sign) :].strip().split(".")
)
ver = tuple(map(int, raw_ver))
yield op_func, ver
break
def validate_required_python_or_fail(python_requires=None):
if python_requires is None:
return
python_version = sys.version_info
preds = parse_predicates(python_requires)
for op, v in preds:
py_ver_slug = python_version[: max(len(v), 3)]
condition_matches = op(py_ver_slug, v)
if not condition_matches:
raise RuntimeError(
"requires Python '{}' but the running Python is {}".format(
python_requires, ".".join(map(str, python_version[:3]))
)
)
def verify_required_python_runtime(s):
@functools.wraps(s)
def sw(**attrs):
try:
validate_required_python_or_fail(attrs.get("python_requires"))
except RuntimeError as re:
sys.exit("{} {!s}".format(attrs["name"], re))
return s(**attrs)
return sw
setuptools.setup = ignore_unknown_options(setuptools.setup)
setuptools.setup = verify_required_python_runtime(setuptools.setup)
try:
from configparser import ConfigParser, NoSectionError
except ImportError:
from ConfigParser import ConfigParser, NoSectionError
ConfigParser.read_file = ConfigParser.readfp
def maybe_read_files(d):
"""Read files if the string starts with `file:` marker."""
FILE_FUNC_MARKER = "file:"
d = d.strip()
if not d.startswith(FILE_FUNC_MARKER):
return d
descs = []
for fname in map(str.strip, str(d[len(FILE_FUNC_MARKER) :]).split(",")):
with io.open(fname, encoding="utf-8") as f:
descs.append(f.read())
return "".join(descs)
def cfg_val_to_list(v):
"""Turn config val to list and filter out empty lines."""
return list(filter(bool, map(str.strip, str(v).strip().splitlines())))
def cfg_val_to_dict(v):
"""Turn config val to dict and filter out empty lines."""
return dict(
map(
lambda l: list(map(str.strip, l.split("=", 1))),
filter(bool, map(str.strip, str(v).strip().splitlines())),
)
)
def cfg_val_to_primitive(v):
"""Parse primitive config val to appropriate data type."""
return json.loads(v.strip().lower())
def read_configuration(filepath):
"""Read metadata and options from setup.cfg located at filepath."""
cfg = ConfigParser()
with io.open(filepath, encoding="utf-8") as f:
cfg.read_file(f)
md = dict(cfg.items("metadata"))
for list_key in "classifiers", "keywords", "project_urls":
try:
md[list_key] = cfg_val_to_list(md[list_key])
except KeyError:
pass
try:
md["long_description"] = maybe_read_files(md["long_description"])
except KeyError:
pass
opt = dict(cfg.items("options"))
for list_key in "include_package_data", "use_scm_version", "zip_safe":
try:
opt[list_key] = cfg_val_to_primitive(opt[list_key])
except KeyError:
pass
for list_key in "scripts", "install_requires", "setup_requires":
try:
opt[list_key] = cfg_val_to_list(opt[list_key])
except KeyError:
pass
try:
opt["package_dir"] = cfg_val_to_dict(opt["package_dir"])
except KeyError:
pass
try:
opt_package_data = dict(cfg.items("options.package_data"))
if not opt_package_data.get("", "").strip():
opt_package_data[""] = opt_package_data["*"]
del opt_package_data["*"]
except (KeyError, NoSectionError):
opt_package_data = {}
try:
opt_extras_require = dict(cfg.items("options.extras_require"))
opt["extras_require"] = {}
for k, v in opt_extras_require.items():
opt["extras_require"][k] = cfg_val_to_list(v)
except NoSectionError:
pass
opt["package_data"] = {}
for k, v in opt_package_data.items():
opt["package_data"][k] = cfg_val_to_list(v)
try:
opt_exclude_package_data = dict(cfg.items("options.exclude_package_data"))
if (
not opt_exclude_package_data.get("", "").strip()
and "*" in opt_exclude_package_data
):
opt_exclude_package_data[""] = opt_exclude_package_data["*"]
del opt_exclude_package_data["*"]
except NoSectionError:
pass
else:
opt["exclude_package_data"] = {}
for k, v in opt_exclude_package_data.items():
opt["exclude_package_data"][k] = cfg_val_to_list(v)
cur_pkgs = opt.get("packages", "").strip()
if "\n" in cur_pkgs:
opt["packages"] = cfg_val_to_list(opt["packages"])
elif cur_pkgs.startswith("find:"):
opt_packages_find = stringify_dict_contents(
dict(cfg.items("options.packages.find"))
)
opt["packages"] = setuptools.find_packages(**opt_packages_find)
return {"metadata": md, "options": opt}
def cut_local_version_on_upload(version):
"""Generate a PEP440 local version if uploading to PyPI."""
import os
import setuptools_scm.version # only present during setup time
IS_PYPI_UPLOAD = os.getenv("PYPI_UPLOAD") == "true" # set in tox.ini
return (
""
if IS_PYPI_UPLOAD
else setuptools_scm.version.get_local_node_and_date(version)
)
if HAS_DIST_INFO_CMD:
class patched_dist_info(setuptools.command.dist_info.dist_info):
def run(self):
self.egg_base = str_if_nested_or_str(self.egg_base)
return setuptools.command.dist_info.dist_info.run(self)
declarative_setup_params = read_configuration("setup.cfg")
"""Declarative metadata and options as read by setuptools."""
setup_params = {}
"""Explicit metadata for passing into setuptools.setup() call."""
setup_params = dict(setup_params, **declarative_setup_params["metadata"])
setup_params = dict(setup_params, **declarative_setup_params["options"])
if HAS_DIST_INFO_CMD:
setup_params["cmdclass"] = {"dist_info": patched_dist_info}
setup_params["use_scm_version"] = {"local_scheme": cut_local_version_on_upload}
# Patch incorrectly decoded package_dir option
# ``egg_info`` demands native strings failing with unicode under Python 2
# Ref https://github.com/pypa/setuptools/issues/1136
setup_params = stringify_dict_contents(setup_params)
if __name__ == "__main__":
setuptools.setup(**setup_params)

102
tox.ini Normal file
View File

@ -0,0 +1,102 @@
[tox]
minversion = 3.12.0
envlist =
lint
check
py{27,35,36,37}-ansible{27,28,29}
doc
devel
skipdist = True
skip_missing_interpreters = True
isolated_build = True
[testenv]
# Hotfix for https://github.com/pypa/pip/issues/6434
# Resolved by https://github.com/tox-dev/tox/issues/1276
install_command =
python -c 'import subprocess, sys; pip_inst_cmd = sys.executable, "-m", "pip", "install"; subprocess.check_call(pip_inst_cmd + ("pip<19.1", )); subprocess.check_call(pip_inst_cmd + tuple(sys.argv[1:]))' {opts} {packages}
usedevelop = True
passenv = *
setenv =
ANSIBLE_CALLABLE_WHITELIST={env:ANSIBLE_CALLABLE_WHITELIST:timer,profile_roles}
PYTHONDONTWRITEBYTECODE=1
deps =
ansible27: ansible>=2.7,<2.8
ansible28: ansible>=2.8,<2.9
ansible29: ansible>=2.9.0b1,<2.10
devel: ansible>=2.9.0b1
devel: docker
extras =
test
commands =
pip check
devel: pip install -e "git+https://github.com/ansible/molecule.git#egg=molecule"
python -m pytest {posargs}
whitelist_externals =
find
sh
[testenv:lint]
commands =
# to run a single linter you can do "pre-commit run flake8"
python -m pre_commit run {posargs:--all}
deps = pre-commit>=1.18.1
extras =
skip_install = true
usedevelop = false
[testenv:check]
envdir = {toxworkdir}/py36-ansible28
deps =
{[testenv]deps}
collective.checkdocs==0.2
twine==1.14.0
usedevelop = False
commands =
python -m pytest --collect-only
python -m setup checkdocs check --metadata --restructuredtext --strict --verbose
python -m twine check .tox/dist/*
[testenv:build-docker]
platform = ^darwin|^linux
usedevelop = False
skip_install = True
deps =
setuptools_scm==3.3.3
packaging # pyup: ignore
commands_pre =
commands =
python ./utils/build-docker.py
whitelist_externals =
sh
[testenv:build-dists-local]
description =
Generate dists which may be not ready
for upload to PyPI because of
containing PEP440 local version part
usedevelop = false
skip_install = true
deps =
pep517 >= 0.5.0
setenv =
commands =
python -m pep517.build \
--source \
--binary \
--out-dir {toxinidir}/dist/ \
{toxinidir}
[testenv:build-dists]
description = Generate dists ready for upload to PyPI
usedevelop = {[testenv:build-dists-local]usedevelop}
skip_install = {[testenv:build-dists-local]skip_install}
deps = {[testenv:build-dists-local]deps}
setenv =
PYPI_UPLOAD = true
commands =
rm -rfv {toxinidir}/dist/
{[testenv:build-dists-local]commands}
whitelist_externals =
rm
{[testenv]whitelist_externals}