diff --git a/LICENSE b/LICENSE index 0a04128..a27c486 100644 --- a/LICENSE +++ b/LICENSE @@ -1,165 +1,19 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. +Copyright © 2020-2021 Adam Weeden + +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. diff --git a/Makefile b/Makefile index 25d3505..b15a3cd 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ twine-upload: twine-check upgrade-pip: PIP_REQUIRE_VIRTUALENV=$(REQUIRE_PIP) pip install --disable-pip-version-check upgrade-ensurepip PIP_REQUIRE_VIRTUALENV=$(REQUIRE_PIP) python -m upgrade_ensurepip + PIP_REQUIRE_VIRTUALENV=$(REQUIRE_PIP) python -m pip install pip --upgrade ACT_EXISTS := $(shell act --help 2> /dev/null) diff --git a/README.md b/README.md index bf5ca57..32560a4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ in the `requires` list. Replace setuptools import in your `setup.py` with an import of ppsetuptools. ppsetuptools exposes all functions from setuptools, and in addition will map -your `pyproject.toml` data to the call to `setuptools.setup` for you. +your `pyproject.toml` data to the call to `setuptools.setup` for you (via +PEP-621 compliant entries). ### Example `pyproject.toml` @@ -22,7 +23,7 @@ your `pyproject.toml` data to the call to `setuptools.setup` for you. name = 'my_package' project_name = 'my_package' version = '1.0.0' -long_description = 'file: README.md' +readme = 'README.md' install_requires = [ 'setuptools', 'toml' @@ -46,20 +47,16 @@ from ppsetuptools import setup setup() ``` -### File references - -ppsetuptools will attempt to replace any strings beginning with "file:" with the -file's contents. For the long_description entry, ppsetuptools will also attempt -to fill long_description_content_type for you based on the filename. - ### File locations As of now, the library attempts to find a `pyproject.toml` file in the same directory as the python file that called it. So if calling directly from `setup.py`, ensure that your `pyproject.toml` file is in the same directory. -As well any file references will attempt to be followed from this location. -E.g., if including a `file: README.md` reference, ppsetuptools will look for +As well any file references (such as the `readme`) will attempt to be followed +from this location. + +E.g., if including a `redme = 'README.md'` value, ppsetuptools will look for `README.md` in the same directory as the file that called it. ### Function support @@ -77,8 +74,3 @@ setup( find_packages(exclude=['tests']) ) ``` - -## PEP 621 - -NOTE: This is not currently PEP 621 as that PEP is still in draft status. This -project will be made PEP 621 compliant in the future if the PEP is accepted. diff --git a/ppsetuptools/ppsetuptools.py b/ppsetuptools/ppsetuptools.py index b95135a..d028307 100644 --- a/ppsetuptools/ppsetuptools.py +++ b/ppsetuptools/ppsetuptools.py @@ -1,9 +1,7 @@ -# pylint:disable = unused-wildcard-import -import codecs import inspect import mimetypes from os import path -from typing import Any, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import setuptools import toml @@ -13,14 +11,99 @@ 'md': 'text/markdown' } -valid_setup_params = ['name', 'version', 'description', 'long_description', 'long_description_content_type', 'url', - 'author', 'author_email', 'maintainer', 'maintainer_email', 'license', 'classifiers', 'keywords', - 'install_requires', 'include_package_data', 'extras_require', 'zip_safe', 'packages', 'scripts', - 'package_data', 'data_files', 'entry_points'] +valid_setup_params = ['name', 'version', 'description', 'long_description', 'long_description_content_type', 'author', + 'author_email', 'maintainer', 'maintainer_email', 'url', 'download_url', 'packages', + 'py_modules', 'scripts', 'ext_package', 'ext_modules', 'classifiers', 'distclass', 'script_name', + 'script_args', 'options', 'license', 'license_files', 'keywords', 'platforms', 'cmdclass', + 'package_dir', 'include_package_data', 'exclude_package_data', 'package_data', 'zip_safe', + 'install_requires', 'entry_points', 'extras_require', 'python_requires', 'namespace_packages', + 'test_suite', 'tests_require', 'test_loader', 'eager_resources', 'use_2to3', + 'convert_2to3_doctests', 'use_2to3_fixers', 'use_2to3_exclude_fixers', 'project_urls'] __all__ = setuptools.__all__ -open = codecs.open # pylint:disable=redefined-builtin +_SETUPTOOLS_OUTPUT_PARAMS = Union[str, Tuple[str, str]] # pylint: disable=invalid-name +_SETUPTOOLS_OUTPUT_BASE_TYPES = Optional[Union[str, Dict[str, Any], List[str]]] # pylint: disable=invalid-name +_SETUPTOOLS_OUTPUT_TYPES = Union[ # pylint: disable=invalid-name + _SETUPTOOLS_OUTPUT_BASE_TYPES, + Tuple[_SETUPTOOLS_OUTPUT_BASE_TYPES, _SETUPTOOLS_OUTPUT_BASE_TYPES] +] +_SETUPTOOLS_TRANSFORM_FUNCTION = Callable[..., _SETUPTOOLS_OUTPUT_TYPES] # pylint: disable=invalid-name + + +def _no_transform(value: _SETUPTOOLS_OUTPUT_TYPES) -> _SETUPTOOLS_OUTPUT_TYPES: + return value + + +def _contributor_transform(contributors: List[Dict[str, str]]) -> Tuple[Optional[str], Optional[str]]: + contributor_names = [] + contributor_emails = [] + if contributors and isinstance(contributors, list): + for contributor in contributors: + if isinstance(contributor, dict): + name = contributor.get('name') + email = contributor.get('email') + if name and email: + contributor_names.append('{} <{}>'.format(name, email)) + elif name: + contributor_names.append(name) + elif email: + contributor_emails.append(email) + return (','.join(contributor_names) or None, ','.join(contributor_emails) or None) + + +def _join_list_transform(value: List[str]) -> Optional[str]: + if not value: + return None + + return ','.join(value) + + +def _license_transform(license_value: Union[str, Dict[str, str]]) -> Tuple[Optional[str], Optional[List[str]]]: + if not license_value: + return (None, None) + + if isinstance(license_value, str): + license_file = None + license_text: Optional[str] = license_value + elif isinstance(license_value, dict): + license_file = license_value.get('file') + license_text = license_value.get('text') + else: + raise ValueError( + "Expected pyproject.toml value for project.license to be a dictionary. Got {}".format(type(license_value))) + if license_file and license_text: + raise ValueError( + 'project.license should contain either file or text, not both.') + + if license_file: + return (None, [license_file]) + + return (license_text, None) + + +def _readme_transform(readme_value: str, caller_directory: str) -> Tuple[Optional[str], Optional[str]]: + if not readme_value: + return (readme_value, None) + + long_description = _replace_file(readme_value, caller_directory) + long_description_content_type = _get_mimetype(readme_value) + + return long_description, long_description_content_type + + +_pyproject_setuptools_mapping: Dict[str, Tuple[_SETUPTOOLS_OUTPUT_PARAMS, _SETUPTOOLS_TRANSFORM_FUNCTION]] = { + 'readme': (('long_description', 'long_description_content_type'), _readme_transform), + 'requires-python': ('python_requires', _no_transform), + 'license': (('license', 'license_files'), _license_transform), + 'authors': (('author', 'author_email'), _contributor_transform), + 'maintainers': (('maintainer', 'maintainer_email'), _contributor_transform), + 'keywords': ('keywords', _join_list_transform), + 'urls': ('project_urls', _no_transform), + 'entry-points': ('entry_points', _no_transform), + 'dependencies': ('install_requires', _no_transform), + 'optional-dependencies': ('extras_require', _no_transform), +} def setup(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: # pylint: disable=function-redefined @@ -29,34 +112,20 @@ def setup(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: # pylint: disable except: # pylint: disable=bare-except caller_directory = '.' - with open(path.join(caller_directory, 'pyproject.toml'), 'r', encoding='utf-8') as pptoml: - pyproject_toml = pptoml.read() + with open(path.join(caller_directory, 'pyproject.toml'), 'r') as pptoml: + pyproject_toml: Union[str, bytes] = pptoml.read() + if isinstance(pyproject_toml, bytes): # pragma: no cover + pyproject_toml = pyproject_toml.decode('utf-8') pyproject_data = toml.loads(pyproject_toml) - # Treat dependencies as install_requires - dependencies = pyproject_data["project"].get("dependencies") - if dependencies: - install_requires = pyproject_data["project"].get("install_requires", []) - pyproject_data["project"]["install_requires"] = list(set(install_requires + dependencies)) - - # Treat optional-dependencies as extra_requires - optionals = pyproject_data["project"].get("optional-dependencies") - if optionals: - extras = pyproject_data["project"].get("extras_require", {}) - optionals.update(extras) - pyproject_data["project"]["extras_require"] = optionals - - kwargs_copy = kwargs.copy() - kwargs_copy.update(_filter_dict(pyproject_data['project'], valid_setup_params)) - - kwargs = _parse_kwargs(kwargs_copy, caller_directory) + parsed_kwargs = _parse_kwargs(pyproject_data['project'], caller_directory) + parsed_kwargs.update(kwargs) - print('Calling setuptools.setup with args: {}'.format(args)) - print('And kwargs:') - print(kwargs) + print('Calling setuptools.setup with args:\n', args) + print('And kwargs:\n', parsed_kwargs) - return setuptools.setup(*args, **kwargs) + return setuptools.setup(*args, **parsed_kwargs) def _filter_dict(kwargs: Dict[str, Any], allowed_params: List[str]) -> Dict[str, Any]: @@ -64,38 +133,44 @@ def _filter_dict(kwargs: Dict[str, Any], allowed_params: List[str]) -> Dict[str, def _parse_kwargs(kwargs: Dict[str, Any], caller_directory: str) -> Dict[str, Any]: - long_description = kwargs.get('long_description') - if long_description and long_description.startswith('file:'): - kwargs['long_description_content_type'] = _get_mimetype(long_description.split('file:')[-1].lower()) - print('long_description is a file reference: "{}"'.format(long_description)) - print('Assigning long_description_content_type of "{}"'.format( - kwargs['long_description_content_type']) - ) - kwargs = _replace_files(kwargs, caller_directory) - return kwargs - - -def _replace_files(kwargs: Dict[str, Any], caller_directory: str) -> Dict[str, Any]: - for key, value in kwargs.items(): - if isinstance(kwargs[key], str) and kwargs[key].startswith('file:'): - try: - filename = kwargs[key].split('file:')[-1].strip() - with open(path.join(caller_directory, filename), 'r', encoding='utf-8') as toml_file_link: - kwargs[key] = toml_file_link.read().replace('\r\n', '\n') - except: # pylint: disable=bare-except - # If we failed, just keep the value - pass - elif isinstance(value, dict): - kwargs[key] = _replace_files(kwargs[key], caller_directory) - - return kwargs + # Pre-include actual setuptools kwargs like name, which does not need to be transformed + kwargs_parsed: Dict[str, Any] = _filter_dict(kwargs, valid_setup_params) + + for key, (output_keys, transform_func) in _pyproject_setuptools_mapping.items(): + value = kwargs.get(key) + transform_signature = inspect.signature(transform_func) + if 'caller_directory' in transform_signature.parameters.keys(): + result = transform_func(value, caller_directory) # pylint: disable=not-callable + else: + result = transform_func(value) # pylint: disable=not-callable + if isinstance(output_keys, tuple): + if not isinstance(result, tuple) or len(result) != len(output_keys): + raise ValueError('Expected {} values to return from {} , but received {}'.format( # pragma: no cover + len(output_keys), + transform_func.__name__, # pylint: disable=no-member + len(result) if isinstance(result, tuple) else 1) + ) + for index, output_key in enumerate(output_keys): + kwargs_parsed[output_key] = result[index] + else: + kwargs_parsed[output_keys] = result + + return kwargs_parsed + + +def _replace_file(filename: str, caller_directory: str) -> str: + with open(path.join(caller_directory, filename), 'r', encoding='utf-8') as file_link: + file_data: Union[str, bytes] = file_link.read() + if isinstance(file_data, bytes): + file_data = file_data.decode('utf-8') # pragma: no cover + return file_data.replace('\r\n', '\n') def _get_mimetype(filename: str) -> Optional[str]: - mimetype = mimetypes.guess_type(filename) + mimetype = mimetypes.guess_type(filename.lower()) if mimetype and mimetype[0]: return mimetype[0] - extension = filename.split('.')[-1] + extension = filename.split('.')[-1].lower() return mimetype_overrides.get(extension) diff --git a/pyproject.toml b/pyproject.toml index 73b8c4b..febb430 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,13 +2,6 @@ name = 'ppsetuptools' project_name = 'ppsetuptools' version = '1.0.1' -description = 'Drop in replacement for setuptools that uses pyproject.toml files' -long_description = 'file: README.md' -url = 'https://github.com/TheCleric/ppsetuptools' -author = 'Adam Weeden' -author_email = 'adamweeden@gmail.com' -license = 'LGPL v3' -license_file = 'file: LICENSE' classifiers = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', @@ -19,12 +12,30 @@ classifiers = [ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8' ] -keywords = 'setuptools pyproject setup.py pyproject.toml replacement' +description = 'Drop in replacement for setuptools that uses pyproject.toml files' +include_package_data = true +keywords = [ + 'setuptools', + 'pyproject', + 'setup.py', + 'pyproject.toml', + 'replacement', + 'pep621', + 'pep-621', +] +license = { file = 'LICENSE' } +readme = 'README.md' +requires-python = '>=3.6' +urls = { repository = 'https://github.com/TheCleric/ppsetuptools' } + dependencies = [ 'setuptools>=38.6.0', 'toml', ] -include_package_data = true + +[[project.authors]] +name = 'Adam Weeden' +email = 'adamweeden@gmail.com' [project.optional-dependencies] dev = [ @@ -56,6 +67,9 @@ build-backend = 'setuptools.build_meta' [tool.autopep8] max_line_length = 120 +[tool.isort] +line_length = 120 + [tool.pylint.pep8] 'max-line-length' = 120 diff --git a/tests/ppsetuptools/data/test_pyproject.toml b/tests/ppsetuptools/data/test_pyproject.toml index 6cbccf1..7dacfef 100644 --- a/tests/ppsetuptools/data/test_pyproject.toml +++ b/tests/ppsetuptools/data/test_pyproject.toml @@ -3,29 +3,32 @@ name = 'mock_project' project_name = 'mock_project' version = '0.0.1' description = 'A mock project' -long_description = 'A longer mock project' +readme = 'test_readme.md' url = 'https://github.com/TheCleric/mock_project' -author = 'Me' -author_email = 'me@emample.com' -license = 'LGPL3' +authors = [ + { name = 'Me', email = 'me@emample.com' } +] classifiers = [ 'Mock Project', ] -keywords = 'mock project' +include_package_data = true +keywords = ['mock', 'project'] +license = { text = 'LGPL3' } +maintainers = [ + { email = 'you@yout.com' } +] +requires-python = '>=3.6' install_requires = [ 'setuptools', ] dependencies = [ 'other', ] -include_package_data = true -[project.extras_require] +[project.optional-dependencies] dev = [ 'dev_dependency', ] - -[project.optional-dependencies] test = [ 'test_dependency', ] diff --git a/tests/ppsetuptools/test_ppsetuptools.py b/tests/ppsetuptools/test_ppsetuptools.py index cb9cd8f..7e069cc 100644 --- a/tests/ppsetuptools/test_ppsetuptools.py +++ b/tests/ppsetuptools/test_ppsetuptools.py @@ -1,7 +1,9 @@ +import copy from os import path -from typing import Any, Dict, List +from typing import Any, Callable, Dict, List from unittest.mock import MagicMock, mock_open, patch +import pytest import toml import ppsetuptools.ppsetuptools as ppsetuptools @@ -11,105 +13,224 @@ _HERE = path.abspath(path.dirname(__file__)) -def thrower(*args: List[Any], **kwargs: Dict[str, Any]) -> None: - raise Exception("") +@pytest.fixture(name='thrower') +def _thrower() -> MagicMock: + return MagicMock(side_effect=Exception) def test_setup() -> None: with open(path.join(_HERE, _DATA_DIR, 'test_pyproject.toml'), 'r') as test_toml_file: test_toml_file_contents = test_toml_file.read() + with open(path.join(_HERE, _DATA_DIR, 'test_readme.md'), 'r') as test_readme_file: + readme_contents = test_readme_file.read() + test_toml_data = toml.loads(test_toml_file_contents) - test_toml_data['project']['install_requires'] = list( - set( - test_toml_data['project']['install_requires'] + test_toml_data['project']['dependencies'] - ) - ) + _mock_open = MagicMock(side_effect=[ + mock_open(read_data=test_toml_file_contents)(), + mock_open(read_data=readme_contents)(), + ]) - test_toml_data['project']['optional-dependencies'].update(test_toml_data['project']['extras_require']) - test_toml_data['project']['extras_require'] = test_toml_data['project']['optional-dependencies'] + test_toml_data_copy = copy.copy(test_toml_data) + test_toml_data['project']['readme'] = path.join(_HERE, _DATA_DIR, test_toml_data['project']['readme']) + + expected_params = ppsetuptools._filter_dict( # pylint: disable=protected-access + ppsetuptools._parse_kwargs( # pylint: disable=protected-access + test_toml_data_copy['project'], + path.join(_HERE, _DATA_DIR) + ), + ppsetuptools.valid_setup_params + ) with patch('setuptools.setup', MagicMock()) as setup_mock, \ - patch('builtins.open', mock_open(read_data=test_toml_file_contents.encode('utf-8'))) as _mock_open: + patch('ppsetuptools.ppsetuptools.open', _mock_open): ppsetuptools.setup() - _mock_open.assert_called_once() + assert _mock_open.call_count == 2 setup_mock.assert_called_once_with( - **ppsetuptools._filter_dict(test_toml_data['project'], ppsetuptools.valid_setup_params) # pylint: disable=protected-access + **expected_params ) -def test_setup_inspect_stack_error() -> None: +def test_setup_inspect_stack_error(thrower: Callable[..., Any]) -> None: with open(path.join(_HERE, _DATA_DIR, 'test_pyproject.toml'), 'r') as test_toml_file: test_toml_file_contents = test_toml_file.read() + with open(path.join(_HERE, _DATA_DIR, 'test_readme.md'), 'r') as test_readme_file: + readme_contents = test_readme_file.read() + assert toml.loads(test_toml_file_contents) is not None + _mock_open = MagicMock(side_effect=[ + mock_open(read_data=test_toml_file_contents)(), + mock_open(read_data=readme_contents)(), + ]) + with patch('inspect.stack', thrower), \ patch('setuptools.setup', MagicMock()) as setup_mock, \ - patch('builtins.open', mock_open(read_data=test_toml_file_contents.encode('utf-8'))) as _mock_open: + patch('builtins.open', _mock_open): ppsetuptools.setup() - _mock_open.assert_called_once() + assert _mock_open.call_count == 2 setup_mock.assert_called_once() -def test_replace_files() -> None: - test_filename = 'test_readme.md' - test_toml_dict = { - 'long_description': 'file: ' + path.join(_DATA_DIR, test_filename) - } +def test_replace_file() -> None: + test_filename = path.join(_DATA_DIR, 'test_readme.md') - with open(path.join(_HERE, _DATA_DIR, test_filename), 'r', encoding='utf-8') as test_file: + with open(path.join(_HERE, test_filename), 'r', encoding='utf-8') as test_file: test_file_contents = test_file.read() - result = ppsetuptools._replace_files(test_toml_dict, _HERE) # pylint: disable=protected-access + result = ppsetuptools._replace_file(test_filename, _HERE) # pylint: disable=protected-access + + assert result == test_file_contents + + +def test_replace_file_file_error(thrower: Callable[..., Any]) -> None: + test_filename = path.join(_DATA_DIR, 'test_readme.md') - assert result['long_description'].strip() == test_file_contents.strip() + with pytest.raises(Exception), patch('builtins.open', thrower): + ppsetuptools._replace_file(test_filename, _HERE) # pylint: disable=protected-access -def test_replace_files_deep() -> None: +def test_parse_kwargs() -> None: test_filename = 'test_readme.md' - test_toml_dict = { - 'options': { - 'long_description': 'file: ' + path.join(_DATA_DIR, test_filename) - } + test_toml_dict: Dict[str, Any] = { + 'readme': path.join(_DATA_DIR, test_filename), + 'authors': [{ + 'name': 'Me', + 'email': 'me@me.com' + }], + 'maintainers': [{ + 'name': 'Me2', + 'email': 'me2@me.com' + }] } - with open(path.join(_HERE, _DATA_DIR, test_filename), 'r', encoding='utf-8') as test_file: - test_file_contents = test_file.read() + test_file_content_type = ppsetuptools._get_mimetype(test_filename) # pylint: disable=protected-access - result = ppsetuptools._replace_files(test_toml_dict, _HERE) # pylint: disable=protected-access + with patch('inspect.stack', MagicMock(return_value=[{}, {'filename': path.join(_HERE, 'setup.py')}])): + result = ppsetuptools._parse_kwargs( # pylint: disable=protected-access + test_toml_dict, + _HERE + ) - assert result['options']['long_description'].strip() == test_file_contents.strip() + assert result['long_description'] == ppsetuptools._replace_file( # pylint: disable=protected-access + path.join(_DATA_DIR, test_filename), + _HERE + ) + assert result['long_description_content_type'] == test_file_content_type + assert result['author'] == test_toml_dict['authors'][0]['name'] + " <" + test_toml_dict['authors'][0]['email'] + ">" + assert result['maintainer'] == test_toml_dict['maintainers'][0]['name'] + \ + " <" + test_toml_dict['maintainers'][0]['email'] + ">" -def test_replace_files_file_error() -> None: - test_filename = 'test_readme.md' - test_toml_dict = { - 'long_description': 'file: ' + test_filename +def test_contributor_transform() -> None: + authors = [ + { + 'name': 'Me', + 'email': 'me@me.com' + } + ] + author, author_email = ppsetuptools._contributor_transform(authors) # pylint: disable=protected-access + assert author == 'Me ' + assert author_email is None + + +def test_contributor_transform_no_name() -> None: + authors = [ + { + 'email': 'me@me.com' + } + ] + author, author_email = ppsetuptools._contributor_transform(authors) # pylint: disable=protected-access + assert author is None + assert author_email == 'me@me.com' + + +def test_contributor_transform_no_email() -> None: + authors = [ + { + 'name': 'Me', + } + ] + author, author_email = ppsetuptools._contributor_transform(authors) # pylint: disable=protected-access + assert author == 'Me' + assert author_email is None + + +def test_contributor_transform_no_data() -> None: + authors: List[Dict[str, str]] = [ + { + } + ] + author, author_email = ppsetuptools._contributor_transform(authors) # pylint: disable=protected-access + assert author is None + assert author_email is None + + +def test_license_transform_file() -> None: + license_data = { + 'file': 'license.txt' } + license_text, license_files = ppsetuptools._license_transform(license_data) # pylint: disable=protected-access + assert license_text is None + assert license_files == ['license.txt'] - with patch('ppsetuptools.open', thrower): - result = ppsetuptools._replace_files(test_toml_dict, _HERE) # pylint: disable=protected-access - assert result['long_description'] == 'file: ' + test_filename +def test_license_transform_text() -> None: + license_data = { + 'text': 'LGPL' + } + license_text, license_files = ppsetuptools._license_transform(license_data) # pylint: disable=protected-access + assert license_text == 'LGPL' + assert license_files is None -def test_parse_kwargs() -> None: - test_filename = 'test_readme.md' - test_toml_dict = { - 'long_description': 'file: ' + path.join(_DATA_DIR, test_filename) +def test_license_transform_both() -> None: + license_data = { + 'text': 'LGPL', + 'file': 'license.txt' } + with pytest.raises(ValueError): + ppsetuptools._license_transform(license_data) # pylint: disable=protected-access - here = path.abspath(path.dirname(__file__)) - test_file_content_type = ppsetuptools._get_mimetype(test_filename) # pylint: disable=protected-access +def test_license_transform_old_style() -> None: + license_data = 'LGPL' - result = ppsetuptools._parse_kwargs(test_toml_dict, here) # pylint: disable=protected-access + license_text, license_files = ppsetuptools._license_transform(license_data) # pylint: disable=protected-access + assert license_text == 'LGPL' + assert license_files is None - assert not result['long_description'].startswith('file:') - assert result['long_description_content_type'] == test_file_content_type + +def test_license_transform_invalid() -> None: + license_data: List[Any] = [None] + + with pytest.raises(ValueError): + ppsetuptools._license_transform(license_data) # type: ignore # pylint: disable=protected-access + + +def test_readme_transform() -> None: + readme = path.join(_HERE, _DATA_DIR, 'test_readme.md') + with open(readme) as readme_file: + readme_data = readme_file.read() + + long_description, long_description_content_type = ppsetuptools._readme_transform( # pylint: disable=protected-access + readme, + path.join(_HERE, _DATA_DIR) + ) + assert long_description == readme_data + assert long_description_content_type == 'text/markdown' + + +def test_readme_transform_none() -> None: + long_description, long_description_content_type = ppsetuptools._readme_transform( # pylint: disable=protected-access + None, # type: ignore[arg-type] + path.join(_HERE, _DATA_DIR) + ) + assert long_description is None + assert long_description_content_type is None def test_get_mimetype_markdown() -> None: