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: