diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 0000000000..95cb3eea4e --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1 @@ +ref-names: $Format:%D$ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..a1e5b968e7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,202 @@ +# Force LF line endings for text files +* text=auto eol=lf + +## SOURCE CODE +*.bat text eol=crlf +*.coffee text +*.css text +*.htm text diff=html +*.html text diff=html +*.inc text +*.ini text +*.js text +*.json text +*.jsx text +*.less text +*.od text +*.onlydata text +*.php text diff=php +*.pl text +*.pxd text diff=python +*.py text diff=python +*.py3 text diff=python +*.pyw text diff=python +*.pyx text diff=python +*.rb text diff=ruby +*.sass text +*.scm text +*.scss text +*.sh text eol=lf +*.sql text +*.styl text +*.tag text +*.ts text +*.tsx text +*.vue text +*.xml text +*.xhtml text diff=html + +## DOCKER +*.dockerignore text +Dockerfile text + +## DOCUMENTATION +*.ipynb text +*.markdown text +*.md text +*.mdwn text +*.mdown text +*.mkd text +*.mkdn text +*.mdtxt text +*.mdtext text +*.txt text +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +COPYING text +copyright text +*COPYRIGHT* text +INSTALL text +license text +LICENSE text +NEWS text +readme text +*README* text +TODO text + +## TEMPLATES +*.dot text +*.ejs text +*.haml text +*.handlebars text +*.hbs text +*.hbt text +*.jade text +*.latte text +*.mustache text +*.njk text +*.phtml text +*.tmpl text +*.tpl text +*.twig text + +## LINTERS +.csslintrc text +.eslintrc text +.htmlhintrc text +.jscsrc text +.jshintrc text +.jshintignore text +.stylelintrc text + +## CONFIGS +*.bowerrc text +*.cnf text +*.conf text +*.config text +.babelrc text +.browserslistrc text +.editorconfig text +.env text +.gitattributes text +.gitconfig text +.htaccess text +*.lock text +*.npmignore text +*.yaml text +*.yml text +browserslist text +Makefile text +makefile text + +## HEROKU +Procfile text +.slugignore text + +## GRAPHICS +*.ai binary +*.bmp binary +*.eps binary +*.gif binary +*.ico binary +*.jng binary +*.jp2 binary +*.jpg binary +*.jpeg binary +*.jpx binary +*.jxr binary +*.pdf binary +*.png binary +*.psb binary +*.psd binary +*.svg text +*.svgz binary +*.tif binary +*.tiff binary +*.wbmp binary +*.webp binary + +## AUDIO +*.kar binary +*.m4a binary +*.mid binary +*.midi binary +*.mp3 binary +*.ogg binary +*.ra binary + +## VIDEO +*.3gpp binary +*.3gp binary +*.as binary +*.asf binary +*.asx binary +*.fla binary +*.flv binary +*.m4v binary +*.mng binary +*.mov binary +*.mp4 binary +*.mpeg binary +*.mpg binary +*.ogv binary +*.swc binary +*.swf binary +*.webm binary + +## ARCHIVES +*.7z binary +*.gz binary +*.jar binary +*.rar binary +*.tar binary +*.zip binary + +## FONTS +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary + +## EXECUTABLES +*.exe binary +*.pyc binary + +# Binary files +# ============ +*.db binary +*.p binary +*.pkl binary +*.pyd binary +*.pyo binary + +# Note: .db, .p, and .pkl files are associated +# with the python modules ``pickle``, ``dbm.*``, +# ``shelve``, ``marshal``, ``anydbm``, & ``bsddb`` +# (among others). + +# Needed for setuptools-scm-git-archive: +.git_archival.txt export-subst diff --git a/.travis.yml b/.travis.yml index 11fafd7ba0..51550fca0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -429,7 +429,9 @@ jobs: name: Publishing current Git tagged version of dist to PyPI if: repo == "ansible/molecule" AND tag IS present env: - TOXENV: metadata-validation + TOXENV: metadata-validation,build-dists + before_deploy: + - echo > setup.py deploy: &deploy-step provider: pypi user: ansible-molecule diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 1fa5b379b7..0000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include LICENSE README.rst -include requirements.txt -include test-requirements.txt -include lint-requirements.txt -prune asset -prune tests diff --git a/lint-requirements.txt b/lint-requirements.txt deleted file mode 100644 index da2426b8da..0000000000 --- a/lint-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -flake8>=3.6.0,<4 -yamllint>=1.11.1,<2 diff --git a/molecule/__init__.py b/molecule/__init__.py index 6a7fb08ef3..87a228e8ac 100644 --- a/molecule/__init__.py +++ b/molecule/__init__.py @@ -1,3 +1,4 @@ +# Copyright (c) 2019 Red Hat, Inc. # Copyright (c) 2015-2018 Cisco Systems, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -17,8 +18,13 @@ # 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. +"""Molecule version information.""" -import pbr.version +from __future__ import absolute_import, division, print_function +__metaclass__ = type -version_info = pbr.version.VersionInfo('molecule') # noqa -__version__ = version_info.release_string() +try: + import pkg_resources + __version__ = pkg_resources.get_distribution('molecule').version +except Exception: + __version__ = 'unknown' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..490c397a48 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = [ + "setuptools >= 41.0.0", + "setuptools_scm >= 1.15.0", + "setuptools_scm_git_archive >= 1.0", + "wheel", +] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2e902f49cd..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ --r lint-requirements.txt -ansible-lint>=4.0.2,<5 -anyconfig==0.9.7 -cerberus==1.2 -click==6.7 -click-completion==0.3.1 -colorama==0.3.9 -cookiecutter==1.6.0 -idna<2.8 # because indirect dependency "requests" conflict with it now -python-gilt==1.2.1 -Jinja2==2.10 -pbr==5.1.1 -pexpect==4.6.0 -psutil==5.4.6; sys_platform!="win32" and sys_platform!="cygwin" -PyYAML==3.13 -sh==1.12.14 -six==1.11.0 -tabulate==0.8.2 -testinfra==1.19.0 -tree-format==0.1.2 diff --git a/setup.cfg b/setup.cfg index 8b41c77baa..e2728791d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,17 +1,12 @@ [aliases] dists = clean --all sdist bdist_wheel +[bdist_wheel] +universal = 1 + [metadata] name = molecule -author = Ansible by Red Hat -author-email = info@ansible.com -maintainer = Ansible by Red Hat -maintainer-email = info@ansible.com -summary = Molecule aids in the development and testing of Ansible roles. -license = MIT -description-file = README.rst -home-page = https://github.com/ansible/molecule -requires-python = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.* +url = https://github.com/ansible/molecule project_urls = Bug Tracker = https://github.com/ansible/molecule/issues Release Management = https://github.com/ansible/molecule/projects @@ -20,28 +15,80 @@ project_urls = Documentation = https://molecule.readthedocs.io Mailing lists = https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information Source Code = https://github.com/ansible/molecule -classifier = - Development Status :: 4 - Beta +description = Molecule aids in the development and testing of Ansible roles +long_description = file: README.rst +author = Ansible by Red Hat +author_email = info@ansible.com +maintainer = Ansible by Red Hat +maintainer_email = info@ansible.com +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 + +[options] +use_scm_version = True +python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.* +package_dir = + = . +packages = find: +zip_safe = False + +# These are required during `setup.py` run: +setup_requires = + setuptools_scm >= 1.15.0 + setuptools_scm_git_archive >= 1.0 + +# These are required in actual runtime: +install_requires = + ansible >= 2.5 + ansible-lint >= 4.0.2, < 5 -# setuptools section name: -# [options.extras_require] -# PBR way: -[extras] + anyconfig == 0.9.7 + cerberus >= 1.2, < 3 + click == 6.7 + click-completion == 0.3.1 + colorama == 0.3.9 + cookiecutter == 1.6.0 + python-gilt >= 1.2.1, < 2 + Jinja2 == 2.10 + pexpect >= 4.6.0, < 5 + psutil == 5.4.6; sys_platform!="win32" and sys_platform!="cygwin" + PyYAML == 3.13 + sh == 1.12.14 + six == 1.11.0 + tabulate == 0.8.2 + testinfra == 1.19.0 + tree-format == 0.1.2 + +[options.extras_require] docs = alabaster Sphinx @@ -65,19 +112,14 @@ vagrant = windows = pywinrm -[pbr] -skip_authors = True -skip_changelog = True -warnerrors = True - -[entry_points] +[options.entry_points] console_scripts = - molecule = molecule.shell:main + molecule = molecule.__main__:main + +[options.packages.find] +where = . [build_sphinx] all_files = 1 build-dir = doc/build source-dir = doc/source - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py index 327701cfd2..c8f7b37465 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +#! /usr/bin/env python +# Copyright (c) 2019 Red Hat, Inc. # Copyright (c) 2015-2018 Cisco Systems, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -17,11 +19,228 @@ # 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. +"""Molecule distribution package setuptools installer.""" import setuptools -setuptools.setup( - packages=setuptools.find_packages(), - pbr=True, - setup_requires=['pbr'] +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 = 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<30.3.""" + import io + import json + + 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__, + ) + + 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': + 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 '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) + 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) + + +__name__ == '__main__' and setuptools.setup(**setup_params) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 522e08093e..0000000000 --- a/test-requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -mock==2.0.0 -pur==5.2.1 -pytest==3.6.0 -pytest-cov==2.6.0 -pytest-helpers-namespace==2019.1.8 -pytest-mock==1.10.0 -pytest-verbose-parametrize==1.4.0 -pytest-xdist==1.26.0 -# The error was: KeyError: 'created_at' -shade==1.22.2 -twine -wheel==0.30.0 -yapf>=0.25.0,<2 diff --git a/tox.ini b/tox.ini index 3e1a5580d7..f86f481a41 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 3.7.0 +minversion = 3.8.4 envlist = lint py{27,35,36,37}-ansible{25,26,27}-{functional,unit} @@ -7,6 +7,7 @@ envlist = doc skipdist = True skip_missing_interpreters = True +isolated_build = True [testenv] usedevelop = True @@ -14,9 +15,19 @@ passenv = * setenv = PYTHONDONTWRITEBYTECODE=1 deps = - -rrequirements.txt - -rtest-requirements.txt - ansible24: ansible>=2.4,<2.5 + flake8>=3.6.0,<4 + yamllint>=1.11.1,<2 + + mock==2.0.0 + pytest==3.6.0 + pytest-cov==2.6.0 + pytest-helpers-namespace==2019.1.8 + pytest-mock==1.10.0 + pytest-verbose-parametrize==1.4.0 + pytest-xdist==1.26.0 + shade==1.22.2 + yapf>=0.25.0,<2 + ansible25: ansible>=2.5,<2.6 ansible26: ansible>=2.6,<2.7 ansible27: ansible>=2.7,<2.8 @@ -49,7 +60,8 @@ sitepackages = true [testenv:lint] deps = - -rlint-requirements.txt + flake8>=3.6.0,<4 + yamllint>=1.11.1,<2 extras = skip_install = true usedevelop = false @@ -80,10 +92,6 @@ commands = extras = docs -[testenv:pur] -commands= - pur -r requirements.txt - [testenv:metadata-validation] deps = collective.checkdocs @@ -103,15 +111,13 @@ usedevelop = False skip_install = True # don't install any Python dist deps deps = - pbr -# setuptools_scm + setuptools_scm # reset pre-commands commands_pre = # build the docker container commands = sh -c '\ - TAG=$(python -c "from pbr.packaging import get_version; print(get_version(\".\"))") && \ - #TAG=$(python -c "from setuptools_scm import get_version; print(get_version())") && \ + TAG=$(python -c "from setuptools_scm import get_version; print(get_version())") && \ echo Building version "$TAG"... && \ sudo docker build \ --pull --rm \ @@ -119,3 +125,37 @@ commands = ' 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 = true` overrides `skip_install` instruction, it's unwanted +usedevelop = false +# don't install molecule itself in this env +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}