diff --git a/setup.cfg b/setup.cfg index 376f4c6..1a4647d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ universal = 1 [metadata] -license_file = LICENSE +license_files = LICENSE [zest.releaser] python-file-with-version = src/check_python_versions/__init__.py @@ -13,7 +13,7 @@ include_trailing_comma = true lines_after_imports = 2 reverse_relative = true known_first_party = check_python_versions -known_third_party = pytest, yaml +known_third_party = pytest, yaml, tomlkit skip = check-python-versions [mypy] diff --git a/setup.py b/setup.py index 1580b02..07bc685 100755 --- a/setup.py +++ b/setup.py @@ -68,6 +68,6 @@ 'check-python-versions = check_python_versions.cli:main', ], }, - install_requires=['pyyaml'], + install_requires=['pyyaml', 'tomlkit'], zip_safe=False, ) diff --git a/src/check_python_versions/cli.py b/src/check_python_versions/cli.py index 1ddc494..b1cba9d 100644 --- a/src/check_python_versions/cli.py +++ b/src/check_python_versions/cli.py @@ -104,9 +104,9 @@ def is_package(where: str = '.') -> bool: Does not emit any diagnostics. """ - # TODO: support setup.py-less packages that use pyproject.toml instead setup_py = os.path.join(where, 'setup.py') - return os.path.exists(setup_py) + pyproject_toml = os.path.join(where, 'pyproject.toml') + return os.path.exists(setup_py) or os.path.exists(pyproject_toml) PrintFn = Callable[..., None] @@ -126,7 +126,7 @@ def check_package(where: str = '.', *, print: PrintFn = print) -> bool: return False if not is_package(where): - print("no setup.py -- not a Python package?") + print("no setup.py or pyproject.toml -- not a Python package?") return False return True diff --git a/src/check_python_versions/sources/all.py b/src/check_python_versions/sources/all.py index 254b6f1..b3dd9a2 100644 --- a/src/check_python_versions/sources/all.py +++ b/src/check_python_versions/sources/all.py @@ -1,6 +1,7 @@ from .appveyor import Appveyor from .github import GitHubActions from .manylinux import Manylinux +from .pyproject import PyProject, PyProjectPythonRequires from .setup_py import SetupClassifiers, SetupPythonRequires from .tox import Tox from .travis import Travis @@ -12,6 +13,8 @@ ALL_SOURCES = [ SetupClassifiers, SetupPythonRequires, + PyProject, + PyProjectPythonRequires, Tox, Travis, GitHubActions, diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py new file mode 100644 index 0000000..2cc3a22 --- /dev/null +++ b/src/check_python_versions/sources/pyproject.py @@ -0,0 +1,364 @@ +""" +Support for pyproject.toml. + +There are two ways of declaring Python versions in a pyproject.toml: +classifiers like + + Programming Language :: Python :: 3.8 + +and tool.poetry.dependencies.python keyword. + +check-python-versions supports both. +""" +import io +from io import StringIO +from typing import List, Optional, TextIO, Union, cast + +from tomlkit import TOMLDocument, dumps, load + +from .base import Source +from .setup_py import ( + compute_python_requires, + get_versions_from_classifiers, + parse_python_requires, + update_classifiers, +) +from ..utils import FileLines, FileOrFilename, is_file_object, open_file, warn +from ..versions import SortedVersionList + + +PYPROJECT_TOML = 'pyproject.toml' + +CLASSIFIERS = 'classifiers' +DEPENDENCIES = 'dependencies' +PYTHON = 'python' +PYTHON_REQUIRES = 'requires-python' + +# poetry TOML keywords +TOOL = 'tool' +POETRY = 'poetry' +BUILD_SYSTEM = 'build-system' +BUILD_BACKEND = 'build-backend' +REQUIRES = 'requires' + +# setuptools TOML keywords +PROJECT = 'project' +SETUPTOOLS = 'setuptools' + +# flit TOML keywords +FLIT = 'flit' + + +def load_toml( + filename: FileOrFilename +) -> Optional[TOMLDocument]: + """Utility method that returns a TOMLDocument.""" + table: Optional[TOMLDocument] = None + # tomlkit has two different API to load from file name or file object + if isinstance(filename, str) or isinstance(filename, StringIO): + with open_file(filename) as fp: + table = load(fp) + if isinstance(filename, io.TextIOWrapper): + table = load(filename) + return table + + +def is_poetry_toml( + table: TOMLDocument +) -> bool: + """Utility method to know if pyproject.toml is for poetry.""" + _ret = False + + if TOOL in table: + if POETRY in table[TOOL]: + _ret = True + if BUILD_SYSTEM in table: + if BUILD_BACKEND in table[BUILD_SYSTEM]: + if POETRY in \ + table[BUILD_SYSTEM][BUILD_BACKEND]: + _ret = True + if REQUIRES in table[BUILD_SYSTEM]: + if list(filter(lambda x: POETRY in x, + table[BUILD_SYSTEM][REQUIRES])): + _ret = True + return _ret + + +def is_setuptools_toml( + table: TOMLDocument +) -> bool: + """Utility method to know if pyproject.toml is for setuptool.""" + _ret = False + if BUILD_SYSTEM in table: + if BUILD_BACKEND in table[BUILD_SYSTEM]: + if SETUPTOOLS in \ + table[BUILD_SYSTEM][BUILD_BACKEND]: + _ret = True + if REQUIRES in table[BUILD_SYSTEM]: + if list(filter(lambda x: SETUPTOOLS in x, + table[BUILD_SYSTEM][REQUIRES])): + _ret = True + + # "[tool.setuptools] table is still in beta" + # "These configurations are completely optional + # and probably can be skipped when creating simple packages" + if TOOL in table: + if SETUPTOOLS in table[TOOL]: + _ret = True + return _ret + + +def is_flit_toml( + table: TOMLDocument +) -> bool: + """Utility method to know if pyproject.toml is for flit.""" + _ret = False + if TOOL in table: + if FLIT in table[TOOL]: + _ret = True + if BUILD_SYSTEM in table: + if BUILD_BACKEND in \ + table[BUILD_SYSTEM]: + if FLIT in \ + table[BUILD_SYSTEM][BUILD_BACKEND]: + _ret = True + if REQUIRES in \ + table[BUILD_SYSTEM]: + if list(filter(lambda x: FLIT in x, + table[BUILD_SYSTEM][REQUIRES])): + _ret = True + return _ret + + +def _get_poetry_classifiers( + table: TOMLDocument +) -> List[str]: + if TOOL not in table: + return [] + if POETRY not in \ + table[TOOL]: + return [] + if CLASSIFIERS not in \ + table[TOOL][POETRY]: + return [] + return cast(List[str], table[TOOL][POETRY][CLASSIFIERS]) + + +def _get_setuptools_flit_classifiers( + table: TOMLDocument +) -> List[str]: + if PROJECT not in table: + return [] + if CLASSIFIERS not in \ + table[PROJECT]: + return [] + return cast(List[str], table[PROJECT][CLASSIFIERS]) + + +def _get_pyproject_toml_classifiers( + filename: FileOrFilename = PYPROJECT_TOML +) -> List[str]: + _classifiers = [] + table = load_toml(filename) + if is_poetry_toml(table): + _classifiers = _get_poetry_classifiers(table) + if is_setuptools_toml(table) or is_flit_toml(table): + _classifiers = _get_setuptools_flit_classifiers(table) + + return _classifiers + + +def _get_poetry_python_requires( + table: TOMLDocument +) -> Optional[str]: + if TOOL not in table: + return None + if POETRY not in table[TOOL]: + return None + if DEPENDENCIES not in \ + table[TOOL][POETRY]: + return None + if PYTHON not in \ + table[TOOL][POETRY][DEPENDENCIES]: + return None + return cast(str, table[TOOL][POETRY][DEPENDENCIES][PYTHON]) + + +def _get_setuptools_flit_python_requires( + table: TOMLDocument +) -> Optional[str]: + if PROJECT not in table: + return None + if PYTHON_REQUIRES not in \ + table[PROJECT]: + return None + return cast(str, table[PROJECT][PYTHON_REQUIRES]) + + +def _get_pyproject_toml_python_requires( + filename: FileOrFilename = PYPROJECT_TOML +) -> Optional[str]: + _python_requires = None + table = load_toml(filename) + if is_poetry_toml(table): + _python_requires = _get_poetry_python_requires(table) + if is_setuptools_toml(table) or is_flit_toml(table): + _python_requires = _get_setuptools_flit_python_requires(table) + + return _python_requires + + +def get_supported_python_versions( + filename: FileOrFilename = PYPROJECT_TOML +) -> SortedVersionList: + """Extract supported Python versions from classifiers in pyproject.toml.""" + classifiers = _get_pyproject_toml_classifiers(filename) + + if not classifiers: + # classifier can be an empty list when nothing found + return [] + + if not isinstance(classifiers, list): + warn('The value specified for classifiers is not an array') + return [] + + return get_versions_from_classifiers(classifiers) + + +def get_python_requires( + pyproject_toml: FileOrFilename = PYPROJECT_TOML, +) -> Optional[SortedVersionList]: + """Extract supported Python versions from python_requires in + pyproject.toml.""" + python_requires = _get_pyproject_toml_python_requires(pyproject_toml) + if not python_requires or not isinstance(python_requires, str): + # python_requires can be None + warn('The value specified for python dependency is not a string') + return None + return parse_python_requires(python_requires) + + +def update_supported_python_versions( + filename: FileOrFilename, + new_versions: SortedVersionList, +) -> Optional[FileLines]: + """Update classifiers in a pyproject.toml. + + Does not touch the file but returns a list of lines with new file contents. + """ + classifiers = _get_pyproject_toml_classifiers(filename) + # classifiers is an optional list + if not isinstance(classifiers, list) or not classifiers: + warn('The value specified for classifiers is not an array') + return None + new_classifiers = update_classifiers(classifiers, new_versions) + return _update_pyproject_toml_classifiers(filename, new_classifiers) + + +def update_python_requires( + filename: FileOrFilename, + new_versions: SortedVersionList, +) -> Optional[FileLines]: + """Update python dependency in a pyproject.toml, if it's defined there. + + Does not touch the file but returns a list of lines with new file contents. + """ + python_requires = _get_pyproject_toml_python_requires(filename) + if python_requires is None: + return None + comma = ', ' + if ',' in python_requires and ', ' not in python_requires: + comma = ',' + space = '' + if '> ' in python_requires or '= ' in python_requires: + space = ' ' + new_requires = compute_python_requires( + new_versions, comma=comma, space=space) + if is_file_object(filename): + # Make sure we can read it twice please. + # XXX: I don't like this. + cast(TextIO, filename).seek(0) + return _update_pyproject_python_requires(filename, new_requires) + + +def _set_poetry_classifiers( + table: TOMLDocument, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + table[TOOL][POETRY][CLASSIFIERS] = new_value + _ret = cast(Optional[List[str]], dumps(table).split('\n')) + return _ret + + +def _set_setuptools_flit_classifiers( + table: TOMLDocument, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + table[PROJECT][CLASSIFIERS] = new_value + _ret = cast(Optional[List[str]], dumps(table).split('\n')) + return _ret + + +def _update_pyproject_toml_classifiers( + filename: FileOrFilename, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + _updated_table: Optional[FileLines] = None + table = load_toml(filename) + if is_poetry_toml(table): + _updated_table = _set_poetry_classifiers(table, new_value) + if is_setuptools_toml(table) or is_flit_toml(table): + _updated_table = _set_setuptools_flit_classifiers(table, new_value) + + return _updated_table + + +def _set_poetry_python_requires( + table: TOMLDocument, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + table[TOOL][POETRY][DEPENDENCIES][PYTHON] = new_value + _ret = cast(Optional[FileLines], dumps(table).split('\n')) + return _ret + + +def _set_setuptools_flit_python_requires( + table: TOMLDocument, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + table[PROJECT][PYTHON_REQUIRES] = new_value + _ret = cast(Optional[FileLines], dumps(table).split('\n')) + return _ret + + +def _update_pyproject_python_requires( + filename: FileOrFilename, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + _updated_table: Optional[FileLines] = [] + table = load_toml(filename) + if is_poetry_toml(table): + _updated_table = _set_poetry_python_requires(table, new_value) + if is_setuptools_toml(table) or is_flit_toml(table): + _updated_table = _set_setuptools_flit_python_requires(table, new_value) + + return _updated_table + + +PyProject = Source( + title=PYPROJECT_TOML, + filename=PYPROJECT_TOML, + extract=get_supported_python_versions, + update=update_supported_python_versions, + check_pypy_consistency=True, + has_upper_bound=True, +) + +PyProjectPythonRequires = Source( + title='- python_requires', + filename=PYPROJECT_TOML, + extract=get_python_requires, + update=update_python_requires, + check_pypy_consistency=False, + has_upper_bound=False, # TBH it might have one! +) diff --git a/tests/sources/test_pyproject_flit.py b/tests/sources/test_pyproject_flit.py new file mode 100644 index 0000000..09f044e --- /dev/null +++ b/tests/sources/test_pyproject_flit.py @@ -0,0 +1,342 @@ +import textwrap +from io import StringIO +from tomlkit import dumps +from typing import List + +from check_python_versions.sources.pyproject import ( + get_python_requires, + get_supported_python_versions, + is_flit_toml, + is_poetry_toml, + is_setuptools_toml, + load_toml, + update_python_requires, + update_supported_python_versions, +) +from check_python_versions.utils import ( + FileLines, + FileOrFilename, + open_file, +) +from check_python_versions.versions import Version + + +def v(versions: List[str]) -> List[Version]: + return [Version.from_string(v) for v in versions] + + +def get_toml_content( + filename: FileOrFilename +) -> FileLines: + """Utility method to see if TOML library keeps style and comments.""" + table = load_toml(filename) + return dumps(table).split('\n') + + +def test_get_supported_python_versions(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.10', + ] + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + assert get_supported_python_versions(str(filename)) == \ + v(['2.7', '3.6', '3.10']) + + +def test_get_supported_python_versions_keep_comments(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + # toml comment + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.10', + ] + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + + assert get_toml_content(str(filename)) == \ + ['[project]', + ' name=\'foo\'', + ' # toml comment', + ' classifiers=[', + ' \'Programming Language :: Python :: 2.7\',', + ' \'Programming Language :: Python :: 3.6\',', + ' \'Programming Language :: Python :: 3.10\',', + ' ]', + '[build-system]', + ' requires = ["flit_core >=3.2,<4"]', + ' build-backend = "flit_core.buildapi"', + ''] + + +def test_update_supported_python_versions_not_a_list(tmp_path, capsys): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=''' + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3.6 + ''' + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + assert get_supported_python_versions(str(filename)) == [] + assert ( + "The value specified for classifiers is not an array" + in capsys.readouterr().err + ) + + +def test_update_supported_python_versions_flit(tmp_path, capsys): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 3.6' + ] + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + result = update_supported_python_versions(str(filename), + v(['3.7', '3.8'])) + assert result == ['[project]', + " name='foo'", + ' classifiers=[' + '"Programming Language :: Python :: 3.7", ' + '"Programming Language :: Python :: 3.8"]', + '[build-system]', + ' requires = ["flit_core >=3.2,<4"]', + ' build-backend = "flit_core.buildapi"', + ''] + + +def test_get_python_requires_flit_no_project_table(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [fake-project] + name='foo' + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + with open_file(str(filename)) as _file_obj: + assert get_python_requires(_file_obj) is None + + +def test_supported_python_versions_setuptools_no_classifier(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + with open_file(str(filename)) as _file_obj: + assert get_supported_python_versions(_file_obj) == [] + + +def test_get_python_requires(tmp_path, fix_max_python_3_version): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.6" + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + fix_max_python_3_version(7) + assert get_python_requires(str(pyproject_toml)) == v(['3.6', '3.7']) + fix_max_python_3_version(10) + assert get_python_requires(str(pyproject_toml)) == v([ + '3.6', '3.7', '3.8', '3.9', '3.10', + ]) + + +def test_get_python_requires_not_specified(tmp_path, capsys): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [project] + name='foo' + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + assert get_python_requires(str(pyproject_toml)) is None + assert capsys.readouterr().err.strip() == \ + 'The value specified for python dependency is not a string' + + +def test_get_python_requires_not_a_string(tmp_path, capsys): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [project] + name='foo' + requires-python = [">=3.6"] + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + assert get_python_requires(str(pyproject_toml)) is None + assert ( + 'The value specified for python dependency is not a string' + in capsys.readouterr().err + ) + + +def test_update_python_requires(tmp_path, fix_max_python_3_version): + fix_max_python_3_version(7) + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.4" + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + result = update_python_requires(str(filename), v(['3.5', '3.6', '3.7'])) + assert result is not None + assert "\n".join(result) == textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.5" + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """) + + +def test_update_python_requires_file_object(fix_max_python_3_version): + fix_max_python_3_version(7) + fp = StringIO(textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.4" + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) + assert result is not None + assert "\n".join(result) == textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.5" + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """) + + +def test_update_python_requires_when_missing(capsys): + fp = StringIO(textwrap.dedent("""\ + [project] + name='foo' + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) + assert result is None + assert capsys.readouterr().err == "" + + +def test_update_python_requires_preserves_style(fix_max_python_3_version): + fix_max_python_3_version(2) + fp = StringIO(textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=2.7,!=3.0.*" + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['2.7', '3.2'])) + assert "\n".join(result) == textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=2.7,!=3.0.*,!=3.1.*" + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """) + + +def test_update_python_requires_multiline_error(capsys): + fp = StringIO(textwrap.dedent("""\ + [project] + name='foo' + requires-python = '>=2.7, !=3.0.*' + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['2.7', '3.2'])) + assert result == ['[project]', + " name='foo'", + ' requires-python = ">=2.7, !=3.0.*, !=3.1.*,' + ' !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*, ' + '!=3.8.*, !=3.9.*, !=3.10.*, !=3.11.*"', + '[build-system]', + ' requires = ["flit_core >=3.2,<4"]', + ' build-backend = "flit_core.buildapi"', + ''] + + +def test_flit_toml_from_tools(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.flit.metadata] + module='foo' + """)) + _table = load_toml(str(filename)) + assert is_flit_toml(_table) + assert not is_poetry_toml(_table) + assert not is_setuptools_toml(_table) + + +def test_flit_toml_from_build_backend(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [build-system] + build-backend = "flit_core.buildapi" + """)) + _table = load_toml(str(filename)) + assert is_flit_toml(_table) + assert not is_poetry_toml(_table) + assert not is_setuptools_toml(_table) + + +def test_flit_toml_from_build_requires(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [build-system] + requires = ["flit_core >=3.2,<4"] + """)) + _table = load_toml(str(filename)) + assert is_flit_toml(_table) + assert not is_poetry_toml(_table) + assert not is_setuptools_toml(_table) diff --git a/tests/sources/test_pyproject_poetry.py b/tests/sources/test_pyproject_poetry.py new file mode 100644 index 0000000..e117e1a --- /dev/null +++ b/tests/sources/test_pyproject_poetry.py @@ -0,0 +1,425 @@ +import textwrap +from io import StringIO +from tomlkit import dumps +from typing import List + +from check_python_versions.sources.pyproject import ( + get_python_requires, + get_supported_python_versions, + is_flit_toml, + is_poetry_toml, + is_setuptools_toml, + load_toml, + update_python_requires, + update_supported_python_versions, +) +from check_python_versions.utils import ( + FileLines, + FileOrFilename, + open_file, +) +from check_python_versions.versions import Version + + +def v(versions: List[str]) -> List[Version]: + return [Version.from_string(v) for v in versions] + + +def get_toml_content( + filename: FileOrFilename +) -> FileLines: + """Utility method to see if TOML library keeps style and comments.""" + table = load_toml(filename) + return dumps(table).split('\n') + + +def test_get_supported_python_versions(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.10', + ] + """)) + assert get_supported_python_versions(str(filename)) == \ + v(['2.7', '3.6', '3.10']) + + +def test_get_supported_python_versions_keep_comments(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + # toml comment + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.10', + ] + """)) + + assert get_toml_content(str(filename)) == \ + ['[tool.poetry]', + ' name=\'foo\'', + ' # toml comment', + ' classifiers=[', + ' \'Programming Language :: Python :: 2.7\',', + ' \'Programming Language :: Python :: 3.6\',', + ' \'Programming Language :: Python :: 3.10\',', + ' ]', + ''] + + +def test_update_supported_python_versions_not_a_list(tmp_path, capsys): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + classifiers=''' + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3.6 + ''' + """)) + assert get_supported_python_versions(str(filename)) == [] + assert ( + "The value specified for classifiers is not an array" + in capsys.readouterr().err + ) + + +def test_update_supported_python_versions_poetry(tmp_path, capsys): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 3.6' + ] + """)) + result = update_supported_python_versions(str(filename), + v(['3.7', '3.8'])) + assert result == ['[tool.poetry]', + " name='foo'", + ' classifiers=[' + '"Programming Language :: Python :: 3.7", ' + '"Programming Language :: Python :: 3.8"]', + ''] + + +def test_update_supported_python_versions_not_poetry_table(tmp_path, capsys): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 3.6' + ] + [build-system] + build-backend = "poetry.core.masonry.api" + """)) + result = update_supported_python_versions(str(filename), + v(['3.7', '3.8'])) + assert result is None + assert capsys.readouterr().err.strip() == \ + 'The value specified for classifiers is not an array' + + +def test_update_supported_python_versions_poetry_not_list(tmp_path, capsys): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + """)) + result = update_supported_python_versions(str(filename), + v(['3.7', '3.8'])) + assert result is None + assert capsys.readouterr().err.strip() == \ + 'The value specified for classifiers is not an array' + + +def test_supported_python_versions_poetry_no_tools_table(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + [build-system] + build-backend = "poetry.core.masonry.api" + """)) + with open_file(str(filename)) as _file_obj: + assert get_supported_python_versions(_file_obj) == [] + + +def test_supported_python_versions_poetry_no_poetry_table(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool] + name='foo' + [build-system] + build-backend = "poetry.core.masonry.api" + """)) + with open_file(str(filename)) as _file_obj: + assert get_supported_python_versions(_file_obj) == [] + + +def test_supported_python_versions_poetry_no_classifier(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [build-system] + build-backend = "poetry.core.masonry.api" + """)) + with open_file(str(filename)) as _file_obj: + assert get_supported_python_versions(_file_obj) == [] + + +def test_get_python_requires(tmp_path, fix_max_python_3_version): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">=3.6" + """)) + fix_max_python_3_version(7) + assert get_python_requires(str(pyproject_toml)) == v(['3.6', '3.7']) + fix_max_python_3_version(10) + assert get_python_requires(str(pyproject_toml)) == v([ + '3.6', '3.7', '3.8', '3.9', '3.10', + ]) + + +def test_set_poetry_python_requires(tmp_path, fix_max_python_3_version): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">=3.6" + """)) + fix_max_python_3_version(8) + _ret = update_python_requires(str(pyproject_toml), v(['3.6', '3.7'])) + assert "\n".join(_ret) == textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">=3.6, !=3.8.*" + """) + + +def test_set_poetry_python_requires_with_space( + tmp_path, + fix_max_python_3_version, +): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = "> 3.6" + """)) + fix_max_python_3_version(8) + _ret = update_python_requires(str(pyproject_toml), v(['3.6', '3.7'])) + assert "\n".join(_ret) == textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">= 3.6, != 3.8.*" + """) + + +def test_get_python_requires_not_specified(tmp_path, capsys): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + """)) + assert get_python_requires(str(pyproject_toml)) is None + assert capsys.readouterr().err.strip() == \ + 'The value specified for python dependency is not a string' + + +def test_get_python_requires_not_a_string(tmp_path, capsys): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = [">=3.6"] + """)) + assert get_python_requires(str(pyproject_toml)) is None + assert ( + 'The value specified for python dependency is not a string' + in capsys.readouterr().err + ) + + +def test_get_python_requires_poetry_no_tools_table(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + [build-system] + build-backend = "poetry.core.masonry.api" + """)) + with open_file(str(filename)) as _file_obj: + assert get_python_requires(_file_obj) is None + + +def test_get_python_requires_poetry_no_poetry_table(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool] + name='foo' + [build-system] + build-backend = "poetry.core.masonry.api" + """)) + with open_file(str(filename)) as _file_obj: + assert get_python_requires(_file_obj) is None + + +def test_get_python_requires_poetry_no_python(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + package-one = "^1.0" + """)) + with open_file(str(filename)) as _file_obj: + assert get_python_requires(_file_obj) is None + + +def test_update_python_requires(tmp_path, fix_max_python_3_version): + fix_max_python_3_version(7) + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">=3.4" + """)) + result = update_python_requires(str(filename), v(['3.5', '3.6', '3.7'])) + assert result is not None + assert "\n".join(result) == textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">=3.5" + """) + + +def test_update_python_requires_file_object(fix_max_python_3_version): + fix_max_python_3_version(7) + fp = StringIO(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">=3.4" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) + assert result is not None + assert "\n".join(result) == textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">=3.5" + """) + + +def test_update_python_requires_when_missing(capsys): + fp = StringIO(textwrap.dedent("""\ + [tool.poetry] + name='foo' + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) + assert result is None + assert capsys.readouterr().err == "" + + +def test_update_python_requires_preserves_style(fix_max_python_3_version): + fix_max_python_3_version(2) + fp = StringIO(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">=2.7,!=3.0.*" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['2.7', '3.2'])) + assert "\n".join(result) == textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = ">=2.7,!=3.0.*,!=3.1.*" + """) + + +def test_update_python_requires_multiline_error(capsys): + fp = StringIO(textwrap.dedent("""\ + [tool.poetry] + name='foo' + [tool.poetry.dependencies] + python = '>=2.7, !=3.0.*' + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['2.7', '3.2'])) + assert result == ['[tool.poetry]', + " name='foo'", + ' [tool.poetry.dependencies]', + ' python = ">=2.7, !=3.0.*, !=3.1.*, !=3.3.*,' + ' !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*, ' + '!=3.8.*, !=3.9.*, !=3.10.*, !=3.11.*"', + ''] + + +def test_poetry_toml_from_tools_io_file(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + """)) + with open_file(str(filename)) as _file_obj: + _table = load_toml(_file_obj) + assert is_poetry_toml(_table) + + +def test_poetry_toml_from_tools(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + """)) + _table = load_toml(str(filename)) + assert is_poetry_toml(_table) + assert not is_setuptools_toml(_table) + assert not is_flit_toml(_table) + + +def test_poetry_toml_from_build_backend(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [build-system] + build-backend = "poetry.core.masonry.api" + """)) + _table = load_toml(str(filename)) + assert is_poetry_toml(_table) + assert not is_setuptools_toml(_table) + assert not is_flit_toml(_table) + + +def test_poetry_toml_from_build_requires(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [build-system] + requires = ["poetry-core>=1.0.0"] + """)) + _table = load_toml(str(filename)) + assert is_poetry_toml(_table) + assert not is_setuptools_toml(_table) + assert not is_flit_toml(_table) diff --git a/tests/sources/test_pyproject_setuptools.py b/tests/sources/test_pyproject_setuptools.py new file mode 100644 index 0000000..694acde --- /dev/null +++ b/tests/sources/test_pyproject_setuptools.py @@ -0,0 +1,355 @@ +import textwrap +from io import StringIO +from tomlkit import dumps +from typing import List + +from check_python_versions.sources.pyproject import ( + get_python_requires, + get_supported_python_versions, + is_flit_toml, + is_poetry_toml, + is_setuptools_toml, + load_toml, + update_python_requires, + update_supported_python_versions, +) +from check_python_versions.utils import ( + FileLines, + FileOrFilename, + open_file, +) +from check_python_versions.versions import Version + + +def v(versions: List[str]) -> List[Version]: + return [Version.from_string(v) for v in versions] + + +def get_toml_content( + filename: FileOrFilename +) -> FileLines: + """Utility method to see if TOML library keeps style and comments.""" + table = load_toml(filename) + return dumps(table).split('\n') + + +def test_get_supported_python_versions(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.10', + ] + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + assert get_supported_python_versions(str(filename)) == \ + v(['2.7', '3.6', '3.10']) + + +def test_get_supported_python_versions_keep_comments(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + # toml comment + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.10', + ] + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + + assert get_toml_content(str(filename)) == \ + ['[project]', + ' name=\'foo\'', + ' # toml comment', + ' classifiers=[', + ' \'Programming Language :: Python :: 2.7\',', + ' \'Programming Language :: Python :: 3.6\',', + ' \'Programming Language :: Python :: 3.10\',', + ' ]', + '[build-system]', + ' requires = ["setuptools", "setuptools-scm"]', + ' build-backend = "setuptools.build_meta"', + ''] + + +def test_supported_python_versions_setuptools_no_tools_table(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [fake-table] + name='foo' + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + with open_file(str(filename)) as _file_obj: + assert get_supported_python_versions(_file_obj) == [] + + +def test_update_supported_python_versions_not_a_list(tmp_path, capsys): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=''' + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3.6 + ''' + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + assert get_supported_python_versions(str(filename)) == [] + assert ( + "The value specified for classifiers is not an array" + in capsys.readouterr().err + ) + + +def test_update_supported_python_versions_setuptools(tmp_path, capsys): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 3.6' + ] + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + result = update_supported_python_versions(str(filename), + v(['3.7', '3.8'])) + assert result == ['[project]', + " name='foo'", + ' classifiers=[' + '"Programming Language :: Python :: 3.7", ' + '"Programming Language :: Python :: 3.8"]', + '[build-system]', + ' requires = ["setuptools", "setuptools-scm"]', + ' build-backend = "setuptools.build_meta"', + ''] + + +def test_get_python_requires_setuptools_no_project_table(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [fake-project] + name='foo' + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + with open_file(str(filename)) as _file_obj: + assert get_python_requires(_file_obj) is None + + +def test_supported_python_versions_setuptools_no_classifier(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + with open_file(str(filename)) as _file_obj: + assert get_supported_python_versions(_file_obj) == [] + + +def test_get_python_requires(tmp_path, fix_max_python_3_version): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.6" + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + fix_max_python_3_version(7) + assert get_python_requires(str(pyproject_toml)) == v(['3.6', '3.7']) + fix_max_python_3_version(10) + assert get_python_requires(str(pyproject_toml)) == v([ + '3.6', '3.7', '3.8', '3.9', '3.10', + ]) + + +def test_get_python_requires_not_specified(tmp_path, capsys): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [project] + name='foo' + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + assert get_python_requires(str(pyproject_toml)) is None + assert capsys.readouterr().err.strip() == \ + 'The value specified for python dependency is not a string' + + +def test_get_python_requires_not_a_string(tmp_path, capsys): + pyproject_toml = tmp_path / "pyproject.toml" + pyproject_toml.write_text(textwrap.dedent("""\ + [project] + name='foo' + requires-python = [">=3.6"] + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + assert get_python_requires(str(pyproject_toml)) is None + assert ( + 'The value specified for python dependency is not a string' + in capsys.readouterr().err + ) + + +def test_update_python_requires(tmp_path, fix_max_python_3_version): + fix_max_python_3_version(7) + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.4" + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + result = update_python_requires(str(filename), v(['3.5', '3.6', '3.7'])) + assert result is not None + assert "\n".join(result) == textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.5" + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """) + + +def test_update_python_requires_file_object(fix_max_python_3_version): + fix_max_python_3_version(7) + fp = StringIO(textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.4" + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) + assert result is not None + assert "\n".join(result) == textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=3.5" + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """) + + +def test_update_python_requires_when_missing(capsys): + fp = StringIO(textwrap.dedent("""\ + [project] + name='foo' + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) + assert result is None + assert capsys.readouterr().err == "" + + +def test_update_python_requires_preserves_style(fix_max_python_3_version): + fix_max_python_3_version(2) + fp = StringIO(textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=2.7,!=3.0.*" + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['2.7', '3.2'])) + assert "\n".join(result) == textwrap.dedent("""\ + [project] + name='foo' + requires-python = ">=2.7,!=3.0.*,!=3.1.*" + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """) + + +def test_update_python_requires_multiline_error(capsys): + fp = StringIO(textwrap.dedent("""\ + [project] + name='foo' + requires-python = '>=2.7, !=3.0.*' + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['2.7', '3.2'])) + assert result == ['[project]', + " name='foo'", + ' requires-python = ">=2.7, !=3.0.*, !=3.1.*,' + ' !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*,' + ' !=3.8.*, !=3.9.*, !=3.10.*, !=3.11.*"', + '[build-system]', + ' requires = ["setuptools", "setuptools-scm"]', + ' build-backend = "setuptools.build_meta"', + ''] + + +def test_setuptools_toml_from_tools(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.setuptools.packages] + name='foo' + """)) + _table = load_toml(str(filename)) + assert is_setuptools_toml(_table) + assert not is_poetry_toml(_table) + assert not is_flit_toml(_table) + + +def test_setuptools_toml_from_build_backend(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [build-system] + build-backend = "setuptools.build_meta" + """)) + _table = load_toml(str(filename)) + assert is_setuptools_toml(_table) + assert not is_poetry_toml(_table) + assert not is_flit_toml(_table) + + +def test_setuptools_toml_from_build_requires(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [build-system] + requires = ["setuptools"] + """)) + _table = load_toml(str(filename)) + assert is_setuptools_toml(_table) + assert not is_poetry_toml(_table) + assert not is_flit_toml(_table) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8732aad..4328c02 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -70,6 +70,11 @@ def test_is_package(tmp_path): assert cpv.is_package(tmp_path) +def test_is_package_with_pyproject(tmp_path): + (tmp_path / "pyproject.toml").write_text("") + assert cpv.is_package(tmp_path) + + def test_is_package_no_setup_py(tmp_path): assert not cpv.is_package(tmp_path) @@ -81,7 +86,8 @@ def test_check_not_a_directory(tmp_path, capsys): def test_check_not_a_package(tmp_path, capsys): assert not cpv.check_package(tmp_path) - assert capsys.readouterr().out == 'no setup.py -- not a Python package?\n' + assert capsys.readouterr().out == 'no setup.py or pyproject.toml' \ + ' -- not a Python package?\n' def test_check_package(tmp_path): @@ -151,6 +157,78 @@ def test_check_mismatch(tmp_path, capsys): """) +def test_check_poetry_mismatch(tmp_path, capsys): + poetry_pyproject = tmp_path / "pyproject.toml" + poetry_pyproject.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + ] + """)) + tox_ini = tmp_path / "tox.ini" + tox_ini.write_text(textwrap.dedent("""\ + [tox] + envlist = py27 + """)) + assert cpv.check_versions(tmp_path) is False + assert capsys.readouterr().out == textwrap.dedent("""\ + pyproject.toml says: 2.7, 3.6 + tox.ini says: 2.7 + """) + + +def test_check_setuptools_mismatch(tmp_path, capsys): + setuptools_pyproject = tmp_path / "pyproject.toml" + setuptools_pyproject.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + ] + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + tox_ini = tmp_path / "tox.ini" + tox_ini.write_text(textwrap.dedent("""\ + [tox] + envlist = py27 + """)) + assert cpv.check_versions(tmp_path) is False + assert capsys.readouterr().out == textwrap.dedent("""\ + pyproject.toml says: 2.7, 3.6 + tox.ini says: 2.7 + """) + + +def test_check_flit_mismatch(tmp_path, capsys): + setuptools_pyproject = tmp_path / "pyproject.toml" + setuptools_pyproject.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + ] + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + tox_ini = tmp_path / "tox.ini" + tox_ini.write_text(textwrap.dedent("""\ + [tox] + envlist = py27 + """)) + assert cpv.check_versions(tmp_path) is False + assert capsys.readouterr().out == textwrap.dedent("""\ + pyproject.toml says: 2.7, 3.6 + tox.ini says: 2.7 + """) + + def test_check_mismatch_pypy(tmp_path, capsys): setup_py = tmp_path / "setup.py" setup_py.write_text(textwrap.dedent("""\ @@ -219,6 +297,75 @@ def test_check_only(tmp_path, capsys): """) +def test_poetry_check_only(tmp_path, capsys): + poetry_pyproject = tmp_path / "pyproject.toml" + poetry_pyproject.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + ] + """)) + tox_ini = tmp_path / "tox.ini" + tox_ini.write_text(textwrap.dedent("""\ + [tox] + envlist = py27 + """)) + assert cpv.check_versions(tmp_path, only={'tox.ini'}) + assert capsys.readouterr().out == textwrap.dedent("""\ + tox.ini says: 2.7 + """) + + +def test_setuptools_check_only(tmp_path, capsys): + setuptools_pyproject = tmp_path / "pyproject.toml" + setuptools_pyproject.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + ] + [build-system] + requires = ["setuptools", "setuptools-scm"] + build-backend = "setuptools.build_meta" + """)) + tox_ini = tmp_path / "tox.ini" + tox_ini.write_text(textwrap.dedent("""\ + [tox] + envlist = py27 + """)) + assert cpv.check_versions(tmp_path, only={'tox.ini'}) + assert capsys.readouterr().out == textwrap.dedent("""\ + tox.ini says: 2.7 + """) + + +def test_flit_check_only(tmp_path, capsys): + flit_pyproject = tmp_path / "pyproject.toml" + flit_pyproject.write_text(textwrap.dedent("""\ + [project] + name='foo' + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + ] + [build-system] + requires = ["flit_core >=3.2,<4"] + build-backend = "flit_core.buildapi" + """)) + tox_ini = tmp_path / "tox.ini" + tox_ini.write_text(textwrap.dedent("""\ + [tox] + envlist = py27 + """)) + assert cpv.check_versions(tmp_path, only={'tox.ini'}) + assert capsys.readouterr().out == textwrap.dedent("""\ + tox.ini says: 2.7 + """) + + def test_check_only_glob_source(tmp_path, capsys): setup_py = tmp_path / "setup.py" setup_py.write_text(textwrap.dedent("""\ diff --git a/tox.ini b/tox.ini index f1bd26f..4addd8c 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,7 @@ basepython = python3 skip_install = true deps = mypy + tomlkit types-pyyaml commands = mypy src setup.py {posargs}