From b2e74b5b367377ab61ee74913ec86385e86f2ee8 Mon Sep 17 00:00:00 2001 From: gpongelli Date: Sun, 22 Jan 2023 18:41:35 +0100 Subject: [PATCH 01/39] pyproject.toml poetry file --- pyproject.toml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5b1ada1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[tool.poetry] + name = "check-python-versions" + version = "1.0.0" + homepage = "https://github.com/mgedmin/check-python-versions" + description = "Compare supported Python versions in setup.py vs tox.ini et al." + authors = ["Marius Gedminas "] + readme = "README.rst" + license = "GPL" + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ] + packages = [ + { include = "src" }, + { include = "tests", format = "sdist" }, + ] + + [tool.poetry.dependencies] + python = ">=3.7.0,<4.0" + pyyaml = '*' + + [tool.poetry.scripts] + check-python-versions = 'check_python_versions.cli:main' + + +[build-system] + requires = ["poetry-core>=1.0.0"] + build-backend = "poetry.core.masonry.api" From 7d364955bafde115e09d28eb5424c888f786ce3e Mon Sep 17 00:00:00 2001 From: gpongelli Date: Sun, 22 Jan 2023 18:42:23 +0100 Subject: [PATCH 02/39] search for pyproject.toml file --- src/check_python_versions/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 45540d7b35a8e2150d609c3054d5c7bce094609c Mon Sep 17 00:00:00 2001 From: gpongelli Date: Sun, 22 Jan 2023 18:43:16 +0100 Subject: [PATCH 03/39] add library to parse pyproject and add new file to SOURCES --- setup.py | 2 +- src/check_python_versions/sources/all.py | 3 + .../sources/poetry_pyproject.py | 460 ++++++++++++++++++ 3 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 src/check_python_versions/sources/poetry_pyproject.py diff --git a/setup.py b/setup.py index 1580b02..b4c49cc 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', 'pytomlpp'], zip_safe=False, ) diff --git a/src/check_python_versions/sources/all.py b/src/check_python_versions/sources/all.py index 254b6f1..442ec63 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 .poetry_pyproject import PoetryPyProject, PoetryPyProjectPythonRequires from .setup_py import SetupClassifiers, SetupPythonRequires from .tox import Tox from .travis import Travis @@ -12,6 +13,8 @@ ALL_SOURCES = [ SetupClassifiers, SetupPythonRequires, + PoetryPyProject, + PoetryPyProjectPythonRequires, Tox, Travis, GitHubActions, diff --git a/src/check_python_versions/sources/poetry_pyproject.py b/src/check_python_versions/sources/poetry_pyproject.py new file mode 100644 index 0000000..f9e0e30 --- /dev/null +++ b/src/check_python_versions/sources/poetry_pyproject.py @@ -0,0 +1,460 @@ +""" +Support for pyproject.toml. + +There are two ways of declaring Python versions in a setup.py: +classifiers like + + Programming Language :: Python :: 3.8 + +and tool.poetry.dependencies.python keyword. + +check-python-versions supports both. +""" + +import ast +import os +import re +import shutil +import sys +import pytomlpp + +from functools import partial +from typing import ( + Callable, + Dict, + List, + Optional, + Sequence, + TextIO, + Tuple, + Union, + cast, +) + +from .setup_py import get_versions_from_classifiers +from .base import Source +from ..parsers.python import ( + AstValue, + eval_ast_node, + find_call_kwarg_in_ast, + update_call_arg_in_source, +) +from ..utils import ( + FileLines, + FileOrFilename, + is_file_object, + open_file, + pipe, + warn, +) +from ..versions import ( + MAX_MINOR_FOR_MAJOR, + SortedVersionList, + Version, + VersionList, + expand_pypy, +) + + +PYPROJECT_TOML = 'pyproject.toml' + + +def get_supported_python_versions( + filename: FileOrFilename = PYPROJECT_TOML +) -> SortedVersionList: + """Extract supported Python versions from classifiers in pyproject.toml .""" + + with open_file(filename) as f: + table = pytomlpp.load(f) + + if 'tool' not in table: + return [] + if 'poetry' not in table['tool']: + return [] + if 'classifiers' not in table['tool']['poetry']: + return [] + + classifiers = table['tool']['poetry']['classifiers'] + + if classifiers is None: + # Note: do not return None because setup.py is not an optional source! + # We want errors to show up if setup.py fails to declare Python + # versions in classifiers. + return [] + + return get_versions_from_classifiers(classifiers) + + +def get_python_requires( + setup_py: FileOrFilename = PYPROJECT_TOML, +) -> Optional[SortedVersionList]: + """Extract supported Python versions from python_requires in setup.py.""" + python_requires = get_setup_py_keyword(setup_py, 'python_requires') + if python_requires is None: + return None + if not isinstance(python_requires, str): + warn('The value passed to setup(python_requires=...) is not a string') + return None + return parse_python_requires(python_requires) + + +def is_version_classifier(s: str) -> bool: + """Is this classifier a Python version classifer?""" + prefix = 'Programming Language :: Python :: ' + return s.startswith(prefix) and s[len(prefix):len(prefix) + 1].isdigit() + + +def is_major_version_classifier(s: str) -> bool: + """Is this classifier a major Python version classifer? + + That is, is this a version classifier that omits the minor version? + """ + prefix = 'Programming Language :: Python :: ' + return ( + s.startswith(prefix) + and s[len(prefix):].replace(' :: Only', '').isdigit() + ) + + +def update_classifiers( + classifiers: Sequence[str], + new_versions: SortedVersionList +) -> List[str]: + """Update a list of classifiers with new Python versions.""" + prefix = 'Programming Language :: Python :: ' + + for pos, s in enumerate(classifiers): + if is_version_classifier(s): + break + else: + pos = len(classifiers) + + if any(map(is_major_version_classifier, classifiers)): + new_versions = sorted( + set(new_versions).union( + v._replace(prefix='', minor=-1, suffix='') + for v in new_versions + ) + ) + + classifiers = [ + s for s in classifiers if not is_version_classifier(s) + ] + new_classifiers = [ + f'{prefix}{version}' + for version in new_versions + ] + classifiers[pos:pos] = new_classifiers + return classifiers + + +def update_supported_python_versions( + filename: FileOrFilename, + new_versions: SortedVersionList, +) -> Optional[FileLines]: + """Update classifiers in a setup.py. + + Does not touch the file but returns a list of lines with new file contents. + """ + classifiers = get_setup_py_keyword(filename, 'classifiers') + if classifiers is None: + return None + if not isinstance(classifiers, (list, tuple)): + warn('The value passed to setup(classifiers=...) is not a list') + return None + new_classifiers = update_classifiers(classifiers, new_versions) + return update_setup_py_keyword(filename, 'classifiers', new_classifiers) + + +def update_python_requires( + filename: FileOrFilename, + new_versions: SortedVersionList, +) -> Optional[FileLines]: + """Update python_requires in a setup.py, if it's defined there. + + Does not touch the file but returns a list of lines with new file contents. + """ + python_requires = get_setup_py_keyword(filename, 'python_requires') + 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_python_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_setup_py_keyword(filename, 'python_requires', + new_python_requires) + + +def get_setup_py_keyword( + setup_py: FileOrFilename, + keyword: str, +) -> Optional[AstValue]: + """Extract a value passed to setup() in a setup.py. + + Parses the setup.py into an Abstact Syntax Tree and tries to figure out + what value was passed to the named keyword argument. + + Returns None if the AST is too complicated to statically evaluate. + """ + with open_file(setup_py) as f: + try: + tree = ast.parse(f.read(), f.name) + except SyntaxError as error: + warn(f'Could not parse {f.name}: {error}') + return None + node = find_call_kwarg_in_ast(tree, ('setup', 'setuptools.setup'), keyword, + filename=f.name) + return eval_ast_node(node, keyword) if node is not None else None + + +def update_setup_py_keyword( + setup_py: FileOrFilename, + keyword: str, + new_value: Union[str, List[str]], +) -> FileLines: + """Update a value passed to setup() in a setup.py. + + Does not touch the file but returns a list of lines with new file contents. + """ + with open_file(setup_py) as f: + lines = f.readlines() + new_lines = update_call_arg_in_source(lines, ('setup', 'setuptools.setup'), + keyword, new_value) + return new_lines + + +def parse_python_requires(s: str) -> Optional[SortedVersionList]: + """Compute Python versions allowed by a python_requires expression.""" + + # https://www.python.org/dev/peps/pep-0440/#version-specifiers + rx = re.compile(r'^(~=|==|!=|<=|>=|<|>|===)\s*(\d+(?:\.\d+)*(?:\.\*)?)$') + + class BadConstraint(Exception): + """The version clause is ill-formed according to PEP 440.""" + + # + # This works as follows: we split the specifier on commas into a list + # of Constraints, each represented as a operator and a tuple of numbers + # with a possible trailing '*'. PEP 440 calls them "clauses". + # + + Constraint = Tuple[Union[str, int], ...] + + # + # The we look up a handler for each operartor. This handler takes a + # constraint and compiles it into a checker. A checker is a function + # that takes a Python version number as a 2-tuple and returns True if + # that version passes its constraint. + # + + VersionTuple = Tuple[int, int] + CheckFn = Callable[[VersionTuple], bool] + HandlerFn = Callable[[Constraint], CheckFn] + + # + # Here we're defining the handlers for all the operators + # + + handlers: Dict[str, HandlerFn] = {} + handler = partial(partial, handlers.__setitem__) + + # + # We are not doing a strict PEP-440 implementation here because if + # python_reqiures allows, say, Python 2.7.16, then we want to report that + # as Python 2.7. In each handler ``candidate`` is a two-tuple (X, Y) + # that represents any Python version between X.Y.0 and X.Y.. + # + + @handler('~=') + def compatible_version(constraint: Constraint) -> CheckFn: + """~= X.Y more or less means >= X.Y and == X.Y.*""" + if len(constraint) < 2: + raise BadConstraint('~= requires a version with at least one dot') + if constraint[-1] == '*': + raise BadConstraint('~= does not allow a .*') + return lambda candidate: candidate == constraint[:2] + + @handler('==') + def matching_version(constraint: Constraint) -> CheckFn: + """== X.Y means X.Y, no more, no less; == X[.Y].* is allowed.""" + # we know len(candidate) == 2 + if len(constraint) == 2 and constraint[-1] == '*': + return lambda candidate: candidate[0] == constraint[0] + elif len(constraint) == 1: + # == X should imply Python X.0 + return lambda candidate: candidate == constraint + (0,) + else: + # == X.Y.* and == X.Y.Z both imply Python X.Y + return lambda candidate: candidate == constraint[:2] + + @handler('!=') + def excluded_version(constraint: Constraint) -> CheckFn: + """!= X.Y is the opposite of == X.Y.""" + # we know len(candidate) == 2 + if constraint[-1] != '*': + # != X or != X.Y or != X.Y.Z all are meaningless for us, because + # there exists some W != Z where we allow X.Y.W and thus allow + # Python X.Y. + return lambda candidate: True + elif len(constraint) == 2: + # != X.* excludes the entirety of a major version + return lambda candidate: candidate[0] != constraint[0] + else: + # != X.Y.* excludes one particular minor version X.Y, + # != X.Y.Z.* does not exclude anything, but it's fine, + # len(candidate) != len(constraint[:-1] so it'll be equivalent to + # True anyway. + return lambda candidate: candidate != constraint[:-1] + + @handler('>=') + def greater_or_equal_version(constraint: Constraint) -> CheckFn: + """>= X.Y allows X.Y.* or X.(Y+n).*, or (X+n).*.""" + if constraint[-1] == '*': + raise BadConstraint('>= does not allow a .*') + # >= X, >= X.Y, >= X.Y.Z all work out nicely because in Python + # (3, 0) >= (3,) + return lambda candidate: candidate >= constraint[:2] + + @handler('<=') + def lesser_or_equal_version(constraint: Constraint) -> CheckFn: + """<= X.Y is the opposite of > X.Y.""" + if constraint[-1] == '*': + raise BadConstraint('<= does not allow a .*') + if len(constraint) == 1: + # <= X allows up to X.0 + return lambda candidate: candidate <= constraint + (0,) + else: + # <= X.Y[.Z] allows up to X.Y + return lambda candidate: candidate <= constraint + + @handler('>') + def greater_version(constraint: Constraint) -> CheckFn: + """> X.Y is equivalent to >= X.Y and != X.Y, I think.""" + if constraint[-1] == '*': + raise BadConstraint('> does not allow a .*') + if len(constraint) == 1: + # > X allows X+1.0 etc + return lambda candidate: candidate[:1] > constraint + elif len(constraint) == 2: + # > X.Y allows X.Y+1 etc + return lambda candidate: candidate > constraint + else: + # > X.Y.Z allows X.Y + return lambda candidate: candidate >= constraint[:2] + + @handler('<') + def lesser_version(constraint: Constraint) -> CheckFn: + """< X.Y is equivalent to <= X.Y and != X.Y, I think.""" + if constraint[-1] == '*': + raise BadConstraint('< does not allow a .*') + # < X, < X.Y, < X.Y.Z all work out nicely because in Python + # (3, 0) > (3,), (3, 0) == (3, 0) and (3, 0) < (3, 0, 1) + return lambda candidate: candidate < constraint + + @handler('===') + def arbitrary_version(constraint: Constraint) -> CheckFn: + """=== X.Y means X.Y, without any zero padding etc.""" + if constraint[-1] == '*': + raise BadConstraint('=== does not allow a .*') + # === X does not allow anything + # === X.Y throws me into confusion; will pip compare Python's X.Y.Z === + # X.Y and reject all possible values of Z? + # === X.Y.Z allows X.Y + return lambda candidate: candidate == constraint[:2] + + # + # And now we can do what we planned: split and compile the constraints + # into checkers (which I also call "constraints", for maximum confusion). + # + + constraints: List[CheckFn] = [] + for specifier in map(str.strip, s.split(',')): + m = rx.match(specifier) + if not m: + warn(f'Bad python_requires specifier: {specifier}') + continue + op, arg = m.groups() + ver: Constraint = tuple( + int(segment) if segment != '*' else segment + for segment in arg.split('.') + ) + try: + constraints.append(handlers[op](ver)) + except BadConstraint as error: + warn(f'Bad python_requires specifier: {specifier} ({error})') + + if not constraints: + return None + + # + # And now we can check all the existing Python versions we know about + # and list those that pass all the requirements. + # + + versions = [] + for major in sorted(MAX_MINOR_FOR_MAJOR): + for minor in range(0, MAX_MINOR_FOR_MAJOR[major] + 1): + if all(constraint((major, minor)) for constraint in constraints): + versions.append(Version.from_string(f'{major}.{minor}')) + return versions + + +def compute_python_requires( + new_versions: VersionList, + *, + comma: str = ', ', + space: str = '', +) -> str: + """Compute a value for python_requires that matches a set of versions.""" + new_versions = set(new_versions) + latest_python = Version(major=3, minor=MAX_MINOR_FOR_MAJOR[3]) + if len(new_versions) == 1 and new_versions != {latest_python}: + return f'=={space}{new_versions.pop()}.*' + min_version = min(new_versions) + specifiers = [f'>={space}{min_version}'] + for major in sorted(MAX_MINOR_FOR_MAJOR): + for minor in range(0, MAX_MINOR_FOR_MAJOR[major] + 1): + ver = Version.from_string(f'{major}.{minor}') + if ver >= min_version and ver not in new_versions: + specifiers.append(f'!={space}{ver}.*') + return comma.join(specifiers) + + +def find_python() -> str: + """Find a Python interpreter.""" + # The reason I prefer python3 or python from $PATH over sys.executable is + # this gives the user some control. E.g. if the setup.py of the project + # requires some dependencies, the user could install them into a virtualenv + # and activate it. + if shutil.which('python3'): + return 'python3' + if shutil.which('python'): + return 'python' + return sys.executable + + +PoetryPyProject = 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, +) + +PoetryPyProjectPythonRequires = 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! +) From fd5c470a6238eef1654e6b2c334730bc84d43d21 Mon Sep 17 00:00:00 2001 From: gpongelli Date: Sun, 22 Jan 2023 20:21:47 +0100 Subject: [PATCH 04/39] implement pieces for pyproject.toml handling --- .../sources/poetry_pyproject.py | 400 +++--------------- 1 file changed, 70 insertions(+), 330 deletions(-) diff --git a/src/check_python_versions/sources/poetry_pyproject.py b/src/check_python_versions/sources/poetry_pyproject.py index f9e0e30..2eb4cfd 100644 --- a/src/check_python_versions/sources/poetry_pyproject.py +++ b/src/check_python_versions/sources/poetry_pyproject.py @@ -1,7 +1,7 @@ """ Support for pyproject.toml. -There are two ways of declaring Python versions in a setup.py: +There are two ways of declaring Python versions in a pyproject.toml: classifiers like Programming Language :: Python :: 3.8 @@ -11,14 +11,8 @@ check-python-versions supports both. """ -import ast -import os -import re -import shutil -import sys import pytomlpp -from functools import partial from typing import ( Callable, Dict, @@ -31,39 +25,26 @@ cast, ) -from .setup_py import get_versions_from_classifiers +from .setup_py import get_versions_from_classifiers, parse_python_requires, update_classifiers, compute_python_requires from .base import Source -from ..parsers.python import ( - AstValue, - eval_ast_node, - find_call_kwarg_in_ast, - update_call_arg_in_source, -) from ..utils import ( FileLines, FileOrFilename, is_file_object, open_file, - pipe, warn, ) from ..versions import ( - MAX_MINOR_FOR_MAJOR, SortedVersionList, - Version, - VersionList, - expand_pypy, ) PYPROJECT_TOML = 'pyproject.toml' -def get_supported_python_versions( - filename: FileOrFilename = PYPROJECT_TOML -) -> SortedVersionList: - """Extract supported Python versions from classifiers in pyproject.toml .""" - +def _get_pyproject_toml_classifiers( + filename: FileOrFilename = PYPROJECT_TOML +) -> List[str]: with open_file(filename) as f: table = pytomlpp.load(f) @@ -74,11 +55,35 @@ def get_supported_python_versions( if 'classifiers' not in table['tool']['poetry']: return [] - classifiers = table['tool']['poetry']['classifiers'] + return table['tool']['poetry']['classifiers'] + + +def _get_pyproject_toml_python_requires( + filename: FileOrFilename = PYPROJECT_TOML +) -> List[str]: + with open_file(filename) as f: + table = pytomlpp.load(f) + + if 'tool' not in table: + return [] + if 'poetry' not in table['tool']: + return [] + if 'dependencies' not in table['tool']['poetry']: + return [] + if 'python' not in table['tool']['poetry']['dependencies']: + return [] + return table['tool']['poetry']['dependencies']['python'] + + +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 classifiers is None: - # Note: do not return None because setup.py is not an optional source! - # We want errors to show up if setup.py fails to declare Python + # Note: do not return None because pyproject.toml is not an optional source! + # We want errors to show up if pyproject.toml fails to declare Python # versions in classifiers. return [] @@ -86,95 +91,45 @@ def get_supported_python_versions( def get_python_requires( - setup_py: FileOrFilename = PYPROJECT_TOML, + pyproject_toml: FileOrFilename = PYPROJECT_TOML, ) -> Optional[SortedVersionList]: - """Extract supported Python versions from python_requires in setup.py.""" - python_requires = get_setup_py_keyword(setup_py, 'python_requires') + """Extract supported Python versions from python_requires in pyproject.toml.""" + python_requires = _get_pyproject_toml_python_requires(pyproject_toml) if python_requires is None: return None if not isinstance(python_requires, str): - warn('The value passed to setup(python_requires=...) is not a string') + warn('The value passed to python is not a string') return None return parse_python_requires(python_requires) -def is_version_classifier(s: str) -> bool: - """Is this classifier a Python version classifer?""" - prefix = 'Programming Language :: Python :: ' - return s.startswith(prefix) and s[len(prefix):len(prefix) + 1].isdigit() - - -def is_major_version_classifier(s: str) -> bool: - """Is this classifier a major Python version classifer? - - That is, is this a version classifier that omits the minor version? - """ - prefix = 'Programming Language :: Python :: ' - return ( - s.startswith(prefix) - and s[len(prefix):].replace(' :: Only', '').isdigit() - ) - - -def update_classifiers( - classifiers: Sequence[str], - new_versions: SortedVersionList -) -> List[str]: - """Update a list of classifiers with new Python versions.""" - prefix = 'Programming Language :: Python :: ' - - for pos, s in enumerate(classifiers): - if is_version_classifier(s): - break - else: - pos = len(classifiers) - - if any(map(is_major_version_classifier, classifiers)): - new_versions = sorted( - set(new_versions).union( - v._replace(prefix='', minor=-1, suffix='') - for v in new_versions - ) - ) - - classifiers = [ - s for s in classifiers if not is_version_classifier(s) - ] - new_classifiers = [ - f'{prefix}{version}' - for version in new_versions - ] - classifiers[pos:pos] = new_classifiers - return classifiers - - def update_supported_python_versions( filename: FileOrFilename, new_versions: SortedVersionList, ) -> Optional[FileLines]: - """Update classifiers in a setup.py. + """Update classifiers in a pyproject.toml. Does not touch the file but returns a list of lines with new file contents. """ - classifiers = get_setup_py_keyword(filename, 'classifiers') + classifiers = _get_pyproject_toml_classifiers(filename) if classifiers is None: return None if not isinstance(classifiers, (list, tuple)): warn('The value passed to setup(classifiers=...) is not a list') return None new_classifiers = update_classifiers(classifiers, new_versions) - return update_setup_py_keyword(filename, 'classifiers', new_classifiers) + return _update_pyproject_toml_classifiers(filename, new_classifiers) def update_python_requires( filename: FileOrFilename, new_versions: SortedVersionList, ) -> Optional[FileLines]: - """Update python_requires in a setup.py, if it's defined there. + """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_setup_py_keyword(filename, 'python_requires') + python_requires = _get_pyproject_toml_python_requires(filename) if python_requires is None: return None comma = ', ' @@ -189,256 +144,41 @@ def update_python_requires( # Make sure we can read it twice please. # XXX: I don't like this. cast(TextIO, filename).seek(0) - return update_setup_py_keyword(filename, 'python_requires', - new_python_requires) + return _update_pyproject_toml_python_requires(filename, new_python_requires) + + +def _update_pyproject_toml_classifiers( + filename: FileOrFilename, + new_value: Union[str, List[str]] +) -> dict: + with open_file(filename) as f: + table = pytomlpp.load(f) + if 'tool' not in table: + return {} + if 'poetry' not in table['tool']: + return {} -def get_setup_py_keyword( - setup_py: FileOrFilename, - keyword: str, -) -> Optional[AstValue]: - """Extract a value passed to setup() in a setup.py. + table['tool']['poetry']['classifiers'] = new_value + return table - Parses the setup.py into an Abstact Syntax Tree and tries to figure out - what value was passed to the named keyword argument. - Returns None if the AST is too complicated to statically evaluate. - """ - with open_file(setup_py) as f: - try: - tree = ast.parse(f.read(), f.name) - except SyntaxError as error: - warn(f'Could not parse {f.name}: {error}') - return None - node = find_call_kwarg_in_ast(tree, ('setup', 'setuptools.setup'), keyword, - filename=f.name) - return eval_ast_node(node, keyword) if node is not None else None - - -def update_setup_py_keyword( - setup_py: FileOrFilename, - keyword: str, - new_value: Union[str, List[str]], -) -> FileLines: - """Update a value passed to setup() in a setup.py. +def _update_pyproject_toml_python_requires( + filename: FileOrFilename, + new_value: Union[str, List[str]] +) -> dict: + with open_file(filename) as f: + table = pytomlpp.load(f) - Does not touch the file but returns a list of lines with new file contents. - """ - with open_file(setup_py) as f: - lines = f.readlines() - new_lines = update_call_arg_in_source(lines, ('setup', 'setuptools.setup'), - keyword, new_value) - return new_lines - - -def parse_python_requires(s: str) -> Optional[SortedVersionList]: - """Compute Python versions allowed by a python_requires expression.""" - - # https://www.python.org/dev/peps/pep-0440/#version-specifiers - rx = re.compile(r'^(~=|==|!=|<=|>=|<|>|===)\s*(\d+(?:\.\d+)*(?:\.\*)?)$') - - class BadConstraint(Exception): - """The version clause is ill-formed according to PEP 440.""" - - # - # This works as follows: we split the specifier on commas into a list - # of Constraints, each represented as a operator and a tuple of numbers - # with a possible trailing '*'. PEP 440 calls them "clauses". - # - - Constraint = Tuple[Union[str, int], ...] - - # - # The we look up a handler for each operartor. This handler takes a - # constraint and compiles it into a checker. A checker is a function - # that takes a Python version number as a 2-tuple and returns True if - # that version passes its constraint. - # - - VersionTuple = Tuple[int, int] - CheckFn = Callable[[VersionTuple], bool] - HandlerFn = Callable[[Constraint], CheckFn] - - # - # Here we're defining the handlers for all the operators - # - - handlers: Dict[str, HandlerFn] = {} - handler = partial(partial, handlers.__setitem__) - - # - # We are not doing a strict PEP-440 implementation here because if - # python_reqiures allows, say, Python 2.7.16, then we want to report that - # as Python 2.7. In each handler ``candidate`` is a two-tuple (X, Y) - # that represents any Python version between X.Y.0 and X.Y.. - # - - @handler('~=') - def compatible_version(constraint: Constraint) -> CheckFn: - """~= X.Y more or less means >= X.Y and == X.Y.*""" - if len(constraint) < 2: - raise BadConstraint('~= requires a version with at least one dot') - if constraint[-1] == '*': - raise BadConstraint('~= does not allow a .*') - return lambda candidate: candidate == constraint[:2] - - @handler('==') - def matching_version(constraint: Constraint) -> CheckFn: - """== X.Y means X.Y, no more, no less; == X[.Y].* is allowed.""" - # we know len(candidate) == 2 - if len(constraint) == 2 and constraint[-1] == '*': - return lambda candidate: candidate[0] == constraint[0] - elif len(constraint) == 1: - # == X should imply Python X.0 - return lambda candidate: candidate == constraint + (0,) - else: - # == X.Y.* and == X.Y.Z both imply Python X.Y - return lambda candidate: candidate == constraint[:2] - - @handler('!=') - def excluded_version(constraint: Constraint) -> CheckFn: - """!= X.Y is the opposite of == X.Y.""" - # we know len(candidate) == 2 - if constraint[-1] != '*': - # != X or != X.Y or != X.Y.Z all are meaningless for us, because - # there exists some W != Z where we allow X.Y.W and thus allow - # Python X.Y. - return lambda candidate: True - elif len(constraint) == 2: - # != X.* excludes the entirety of a major version - return lambda candidate: candidate[0] != constraint[0] - else: - # != X.Y.* excludes one particular minor version X.Y, - # != X.Y.Z.* does not exclude anything, but it's fine, - # len(candidate) != len(constraint[:-1] so it'll be equivalent to - # True anyway. - return lambda candidate: candidate != constraint[:-1] - - @handler('>=') - def greater_or_equal_version(constraint: Constraint) -> CheckFn: - """>= X.Y allows X.Y.* or X.(Y+n).*, or (X+n).*.""" - if constraint[-1] == '*': - raise BadConstraint('>= does not allow a .*') - # >= X, >= X.Y, >= X.Y.Z all work out nicely because in Python - # (3, 0) >= (3,) - return lambda candidate: candidate >= constraint[:2] - - @handler('<=') - def lesser_or_equal_version(constraint: Constraint) -> CheckFn: - """<= X.Y is the opposite of > X.Y.""" - if constraint[-1] == '*': - raise BadConstraint('<= does not allow a .*') - if len(constraint) == 1: - # <= X allows up to X.0 - return lambda candidate: candidate <= constraint + (0,) - else: - # <= X.Y[.Z] allows up to X.Y - return lambda candidate: candidate <= constraint - - @handler('>') - def greater_version(constraint: Constraint) -> CheckFn: - """> X.Y is equivalent to >= X.Y and != X.Y, I think.""" - if constraint[-1] == '*': - raise BadConstraint('> does not allow a .*') - if len(constraint) == 1: - # > X allows X+1.0 etc - return lambda candidate: candidate[:1] > constraint - elif len(constraint) == 2: - # > X.Y allows X.Y+1 etc - return lambda candidate: candidate > constraint - else: - # > X.Y.Z allows X.Y - return lambda candidate: candidate >= constraint[:2] - - @handler('<') - def lesser_version(constraint: Constraint) -> CheckFn: - """< X.Y is equivalent to <= X.Y and != X.Y, I think.""" - if constraint[-1] == '*': - raise BadConstraint('< does not allow a .*') - # < X, < X.Y, < X.Y.Z all work out nicely because in Python - # (3, 0) > (3,), (3, 0) == (3, 0) and (3, 0) < (3, 0, 1) - return lambda candidate: candidate < constraint - - @handler('===') - def arbitrary_version(constraint: Constraint) -> CheckFn: - """=== X.Y means X.Y, without any zero padding etc.""" - if constraint[-1] == '*': - raise BadConstraint('=== does not allow a .*') - # === X does not allow anything - # === X.Y throws me into confusion; will pip compare Python's X.Y.Z === - # X.Y and reject all possible values of Z? - # === X.Y.Z allows X.Y - return lambda candidate: candidate == constraint[:2] - - # - # And now we can do what we planned: split and compile the constraints - # into checkers (which I also call "constraints", for maximum confusion). - # - - constraints: List[CheckFn] = [] - for specifier in map(str.strip, s.split(',')): - m = rx.match(specifier) - if not m: - warn(f'Bad python_requires specifier: {specifier}') - continue - op, arg = m.groups() - ver: Constraint = tuple( - int(segment) if segment != '*' else segment - for segment in arg.split('.') - ) - try: - constraints.append(handlers[op](ver)) - except BadConstraint as error: - warn(f'Bad python_requires specifier: {specifier} ({error})') - - if not constraints: - return None + if 'tool' not in table: + return {} + if 'poetry' not in table['tool']: + return {} + if 'dependencies' not in table['tool']['poetry']: + return {} - # - # And now we can check all the existing Python versions we know about - # and list those that pass all the requirements. - # - - versions = [] - for major in sorted(MAX_MINOR_FOR_MAJOR): - for minor in range(0, MAX_MINOR_FOR_MAJOR[major] + 1): - if all(constraint((major, minor)) for constraint in constraints): - versions.append(Version.from_string(f'{major}.{minor}')) - return versions - - -def compute_python_requires( - new_versions: VersionList, - *, - comma: str = ', ', - space: str = '', -) -> str: - """Compute a value for python_requires that matches a set of versions.""" - new_versions = set(new_versions) - latest_python = Version(major=3, minor=MAX_MINOR_FOR_MAJOR[3]) - if len(new_versions) == 1 and new_versions != {latest_python}: - return f'=={space}{new_versions.pop()}.*' - min_version = min(new_versions) - specifiers = [f'>={space}{min_version}'] - for major in sorted(MAX_MINOR_FOR_MAJOR): - for minor in range(0, MAX_MINOR_FOR_MAJOR[major] + 1): - ver = Version.from_string(f'{major}.{minor}') - if ver >= min_version and ver not in new_versions: - specifiers.append(f'!={space}{ver}.*') - return comma.join(specifiers) - - -def find_python() -> str: - """Find a Python interpreter.""" - # The reason I prefer python3 or python from $PATH over sys.executable is - # this gives the user some control. E.g. if the setup.py of the project - # requires some dependencies, the user could install them into a virtualenv - # and activate it. - if shutil.which('python3'): - return 'python3' - if shutil.which('python'): - return 'python' - return sys.executable + table['tool']['poetry']['dependencies']['python'] = new_value + return table PoetryPyProject = Source( From 51c045f06a96d593bed006c85789d445bed3c4de Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 11:15:34 +0100 Subject: [PATCH 05/39] return FileLines instead of dict --- .../sources/poetry_pyproject.py | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/check_python_versions/sources/poetry_pyproject.py b/src/check_python_versions/sources/poetry_pyproject.py index 2eb4cfd..46dbf4e 100644 --- a/src/check_python_versions/sources/poetry_pyproject.py +++ b/src/check_python_versions/sources/poetry_pyproject.py @@ -14,13 +14,9 @@ import pytomlpp from typing import ( - Callable, - Dict, List, Optional, - Sequence, TextIO, - Tuple, Union, cast, ) @@ -150,35 +146,36 @@ def update_python_requires( def _update_pyproject_toml_classifiers( filename: FileOrFilename, new_value: Union[str, List[str]] -) -> dict: +) -> Optional[FileLines]: with open_file(filename) as f: table = pytomlpp.load(f) if 'tool' not in table: - return {} + return [] if 'poetry' not in table['tool']: - return {} + return [] table['tool']['poetry']['classifiers'] = new_value - return table + + return pytomlpp.dumps(table).split('\n') def _update_pyproject_toml_python_requires( filename: FileOrFilename, new_value: Union[str, List[str]] -) -> dict: +) -> Optional[FileLines]: with open_file(filename) as f: table = pytomlpp.load(f) if 'tool' not in table: - return {} + return [] if 'poetry' not in table['tool']: - return {} + return [] if 'dependencies' not in table['tool']['poetry']: - return {} + return [] table['tool']['poetry']['dependencies']['python'] = new_value - return table + return pytomlpp.dumps(table).split('\n') PoetryPyProject = Source( From f54b957ac92d72d060b23001732ae7a577275452 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 11:46:10 +0100 Subject: [PATCH 06/39] first version for tests --- tests/sources/test_poetry_pyproject.py | 289 +++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 tests/sources/test_poetry_pyproject.py diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py new file mode 100644 index 0000000..d8b2a13 --- /dev/null +++ b/tests/sources/test_poetry_pyproject.py @@ -0,0 +1,289 @@ +import textwrap +from io import StringIO +from typing import List + +import pytest + +from check_python_versions.sources.poetry_pyproject import ( + get_python_requires, + get_supported_python_versions, + update_python_requires, + update_supported_python_versions, +) +from check_python_versions.versions import Version + + +def v(versions: List[str]) -> List[Version]: + return [Version.from_string(v) for v in versions] + + +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_computed(tmp_path): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo', + classifiers=[ + 'Programming Language :: Python :: %s' % v + for v in ['2.7', '3.7'] + ] + """)) + assert get_supported_python_versions(str(filename)) == v(['2.7', '3.7']) + + +def test_get_supported_python_versions_string(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 passed to poetry classifiers is not a list" + in capsys.readouterr().err + ) + + +def test_get_supported_python_versions_from_file_object_cannot_run_pyproject_toml(): + fp = StringIO(textwrap.dedent("""\ + [tool.poetry] + name='foo', + classifiers=[ + 'Programming Language :: Python :: %s' % v + for v in ['2.7', '3.7'] + ] + """)) + fp.name = 'pyproject.toml' + assert get_supported_python_versions(fp) == [] + + +def test_update_supported_python_versions_not_literal(tmp_path, capsys): + filename = tmp_path / "pyproject.toml" + filename.write_text(textwrap.dedent("""\ + [tool.poetry] + name='foo', + classifiers=[ + 'Programming Language :: Python :: %s' % v + for v in ['2.7', '3.7'] + ] + """)) + assert update_supported_python_versions(str(filename), + v(['3.7', '3.8'])) is None + assert ( + 'Non-literal classifiers present in poetry_toml' + in capsys.readouterr().err + ) + + +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 update_supported_python_versions(str(filename), + v(['3.7', '3.8'])) is None + assert ( + 'The value passed to classifiers is not a list' + in capsys.readouterr().err + ) + + +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', + classifiers=[ + 'Programming Language :: 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_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 == '' + + +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 passed to 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("""\ + [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 "".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 "".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 "".join(result) == textwrap.dedent("""\ + [tool.poetry] + name='foo', + [tool.poetry.dependencies] + python = ">=2.7,!=3.0.*,!=3.1.*" + """) + + +def test_update_python_requires_multiline(fix_max_python_3_version): + fix_max_python_3_version(2) + fp = StringIO(textwrap.dedent("""\ + [tool.poetry] + name='foo', + [tool.poetry.dependencies] + python = ', '.join([ + '>=2.7', + '!=3.0.*', + ]) + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['2.7', '3.2'])) + assert result is not None + assert "".join(result) == textwrap.dedent("""\ + [tool.poetry] + name='foo', + [tool.poetry.dependencies] + python = ', '.join([ + '>=2.7', + '!=3.0.*', + '!=3.1.*', + ]) + """) + + +def test_update_python_requires_multiline_variations(fix_max_python_3_version): + fix_max_python_3_version(2) + fp = StringIO(textwrap.dedent("""\ + [tool.poetry] + name='foo', + [tool.poetry.dependencies] + python = ",".join([ + ">=2.7", + "!=3.0.*", + ]) + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['2.7', '3.2'])) + assert result is not None + assert "".join(result) == textwrap.dedent("""\ + [tool.poetry] + name='foo', + [tool.poetry.dependencies] + python = ",".join([ + ">=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 = ', '.join([ + '>=2.7', + '!=3.0.*']) + """)) + fp.name = "pyproject.toml" + result = update_python_requires(fp, v(['2.7', '3.2'])) + assert result == fp.getvalue().splitlines(True) + assert ( + "Did not understand python_requires formatting in python dependency" + in capsys.readouterr().err + ) From d8bc619ab7ba19eb4ff6768917c3cf55484825f8 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 12:34:08 +0100 Subject: [PATCH 07/39] build-system seems interfere with appveyor --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5b1ada1..194b175 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,6 @@ check-python-versions = 'check_python_versions.cli:main' -[build-system] - requires = ["poetry-core>=1.0.0"] - build-backend = "poetry.core.masonry.api" +#[build-system] +# requires = ["poetry-core>=1.0.0"] +# build-backend = "poetry.core.masonry.api" From a5a6565853b9d256d884f3d24a621f16d284a973 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 15:09:03 +0100 Subject: [PATCH 08/39] fix deprecation warning --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 376f4c6..f4d8919 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 From b15d091c81f47dfc7da2c3f84e3ef547b4de52d1 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 15:09:50 +0100 Subject: [PATCH 09/39] indentation --- src/check_python_versions/sources/poetry_pyproject.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/check_python_versions/sources/poetry_pyproject.py b/src/check_python_versions/sources/poetry_pyproject.py index 46dbf4e..8f3aebe 100644 --- a/src/check_python_versions/sources/poetry_pyproject.py +++ b/src/check_python_versions/sources/poetry_pyproject.py @@ -39,7 +39,7 @@ def _get_pyproject_toml_classifiers( - filename: FileOrFilename = PYPROJECT_TOML + filename: FileOrFilename = PYPROJECT_TOML ) -> List[str]: with open_file(filename) as f: table = pytomlpp.load(f) @@ -55,7 +55,7 @@ def _get_pyproject_toml_classifiers( def _get_pyproject_toml_python_requires( - filename: FileOrFilename = PYPROJECT_TOML + filename: FileOrFilename = PYPROJECT_TOML ) -> List[str]: with open_file(filename) as f: table = pytomlpp.load(f) @@ -144,8 +144,8 @@ def update_python_requires( def _update_pyproject_toml_classifiers( - filename: FileOrFilename, - new_value: Union[str, List[str]] + filename: FileOrFilename, + new_value: Union[str, List[str]], ) -> Optional[FileLines]: with open_file(filename) as f: table = pytomlpp.load(f) @@ -162,7 +162,7 @@ def _update_pyproject_toml_classifiers( def _update_pyproject_toml_python_requires( filename: FileOrFilename, - new_value: Union[str, List[str]] + new_value: Union[str, List[str]], ) -> Optional[FileLines]: with open_file(filename) as f: table = pytomlpp.load(f) From 08a92d5e4ce7afe9e5e40a491738ec0d26bcdb83 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 15:10:17 +0100 Subject: [PATCH 10/39] fix message when no required files are present --- tests/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8732aad..8d9ea88 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -81,7 +81,7 @@ 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): From 16f0c7d9e832f998e9752802789d31aff2b5b576 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 16:22:56 +0100 Subject: [PATCH 11/39] toml does not need comma --- tests/sources/test_poetry_pyproject.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py index d8b2a13..43953f3 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_poetry_pyproject.py @@ -21,7 +21,7 @@ def test_get_supported_python_versions(tmp_path): filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ [tool.poetry] - name='foo', + name='foo' classifiers=[ 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.6', @@ -48,7 +48,7 @@ def test_get_supported_python_versions_string(tmp_path, capsys): filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ [tool.poetry] - name='foo', + name='foo' classifiers=''' Programming Language :: Python :: 2.7 Programming Language :: Python :: 3.6 @@ -64,7 +64,7 @@ def test_get_supported_python_versions_string(tmp_path, capsys): def test_get_supported_python_versions_from_file_object_cannot_run_pyproject_toml(): fp = StringIO(textwrap.dedent("""\ [tool.poetry] - name='foo', + name='foo' classifiers=[ 'Programming Language :: Python :: %s' % v for v in ['2.7', '3.7'] From 76c40f24e306bdd4a4b9e18142bf4e7826eef34a Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 16:23:44 +0100 Subject: [PATCH 12/39] removed version from toml --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 194b175..6e73386 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,5 @@ [tool.poetry] name = "check-python-versions" - version = "1.0.0" homepage = "https://github.com/mgedmin/check-python-versions" description = "Compare supported Python versions in setup.py vs tox.ini et al." authors = ["Marius Gedminas "] From 483d27b1c3182bd413ef59f36792ba29fa7eee20 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 16:24:19 +0100 Subject: [PATCH 13/39] using tomlkit package --- setup.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index f4d8919..1a4647d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 b4c49cc..07bc685 100755 --- a/setup.py +++ b/setup.py @@ -68,6 +68,6 @@ 'check-python-versions = check_python_versions.cli:main', ], }, - install_requires=['pyyaml', 'pytomlpp'], + install_requires=['pyyaml', 'tomlkit'], zip_safe=False, ) From 0aca98957428b9d2600a6cc3d1cfa1c0df382d43 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 16:25:16 +0100 Subject: [PATCH 14/39] moved to tomlkit, small set of working tests --- .../sources/poetry_pyproject.py | 48 ++- tests/sources/test_poetry_pyproject.py | 313 ++++++++---------- 2 files changed, 180 insertions(+), 181 deletions(-) diff --git a/src/check_python_versions/sources/poetry_pyproject.py b/src/check_python_versions/sources/poetry_pyproject.py index 8f3aebe..a20202a 100644 --- a/src/check_python_versions/sources/poetry_pyproject.py +++ b/src/check_python_versions/sources/poetry_pyproject.py @@ -11,7 +11,8 @@ check-python-versions supports both. """ -import pytomlpp +from tomlkit import dumps +from tomlkit import parse, load from typing import ( List, @@ -38,11 +39,29 @@ PYPROJECT_TOML = 'pyproject.toml' +def _load_toml(filename: FileOrFilename): + table = {} + # tomlkit has two different API to load from file name or file object + if isinstance(filename, str): + with open_file(filename) as fp: + table = load(fp) + if isinstance(filename, TextIO): + table = load(filename) + return table + + +def get_toml_content( + filename: FileOrFilename = PYPROJECT_TOML +) -> FileLines: + """Utility method to see if TOML library keeps style and comments.""" + table = _load_toml(filename) + return dumps(table).split('\n') + + def _get_pyproject_toml_classifiers( filename: FileOrFilename = PYPROJECT_TOML ) -> List[str]: - with open_file(filename) as f: - table = pytomlpp.load(f) + table = _load_toml(filename) if 'tool' not in table: return [] @@ -57,8 +76,7 @@ def _get_pyproject_toml_classifiers( def _get_pyproject_toml_python_requires( filename: FileOrFilename = PYPROJECT_TOML ) -> List[str]: - with open_file(filename) as f: - table = pytomlpp.load(f) + table = _load_toml(filename) if 'tool' not in table: return [] @@ -83,6 +101,10 @@ def get_supported_python_versions( # versions in classifiers. return [] + if not isinstance(classifiers, (list, tuple)): + warn('The value passed to classifiers is not a list') + return [] + return get_versions_from_classifiers(classifiers) @@ -94,7 +116,7 @@ def get_python_requires( if python_requires is None: return None if not isinstance(python_requires, str): - warn('The value passed to python is not a string') + warn('The value passed to python dependency is not a string') return None return parse_python_requires(python_requires) @@ -111,7 +133,7 @@ def update_supported_python_versions( if classifiers is None: return None if not isinstance(classifiers, (list, tuple)): - warn('The value passed to setup(classifiers=...) is not a list') + warn('The value passed to classifiers is not a list') return None new_classifiers = update_classifiers(classifiers, new_versions) return _update_pyproject_toml_classifiers(filename, new_classifiers) @@ -126,7 +148,7 @@ def update_python_requires( 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: + if python_requires is None or python_requires == []: return None comma = ', ' if ',' in python_requires and ', ' not in python_requires: @@ -147,8 +169,7 @@ def _update_pyproject_toml_classifiers( filename: FileOrFilename, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - with open_file(filename) as f: - table = pytomlpp.load(f) + table = _load_toml(filename) if 'tool' not in table: return [] @@ -157,15 +178,14 @@ def _update_pyproject_toml_classifiers( table['tool']['poetry']['classifiers'] = new_value - return pytomlpp.dumps(table).split('\n') + return dumps(table).split('\n') def _update_pyproject_toml_python_requires( filename: FileOrFilename, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - with open_file(filename) as f: - table = pytomlpp.load(f) + table = _load_toml(filename) if 'tool' not in table: return [] @@ -175,7 +195,7 @@ def _update_pyproject_toml_python_requires( return [] table['tool']['poetry']['dependencies']['python'] = new_value - return pytomlpp.dumps(table).split('\n') + return dumps(table).split('\n') PoetryPyProject = Source( diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py index 43953f3..5bf4f2a 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_poetry_pyproject.py @@ -7,6 +7,7 @@ from check_python_versions.sources.poetry_pyproject import ( get_python_requires, get_supported_python_versions, + get_toml_content, update_python_requires, update_supported_python_versions, ) @@ -31,17 +32,28 @@ def test_get_supported_python_versions(tmp_path): assert get_supported_python_versions(str(filename)) == v(['2.7', '3.6', '3.10']) -def test_get_supported_python_versions_computed(tmp_path): +def test_get_supported_python_versions_keep_comments(tmp_path): filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ [tool.poetry] - name='foo', + name='foo' + # toml comment classifiers=[ - 'Programming Language :: Python :: %s' % v - for v in ['2.7', '3.7'] + '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.7']) + + 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_get_supported_python_versions_string(tmp_path, capsys): @@ -56,68 +68,37 @@ def test_get_supported_python_versions_string(tmp_path, capsys): """)) assert get_supported_python_versions(str(filename)) == [] assert ( - "The value passed to poetry classifiers is not a list" + "The value passed to classifiers is not a list" in capsys.readouterr().err ) -def test_get_supported_python_versions_from_file_object_cannot_run_pyproject_toml(): - fp = StringIO(textwrap.dedent("""\ - [tool.poetry] - name='foo' - classifiers=[ - 'Programming Language :: Python :: %s' % v - for v in ['2.7', '3.7'] - ] - """)) - fp.name = 'pyproject.toml' - assert get_supported_python_versions(fp) == [] - - -def test_update_supported_python_versions_not_literal(tmp_path, capsys): - filename = tmp_path / "pyproject.toml" - filename.write_text(textwrap.dedent("""\ - [tool.poetry] - name='foo', - classifiers=[ - 'Programming Language :: Python :: %s' % v - for v in ['2.7', '3.7'] - ] - """)) - assert update_supported_python_versions(str(filename), - v(['3.7', '3.8'])) is None - assert ( - 'Non-literal classifiers present in poetry_toml' - in capsys.readouterr().err - ) - - -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 update_supported_python_versions(str(filename), - v(['3.7', '3.8'])) is None - assert ( - 'The value passed to classifiers is not a list' - in capsys.readouterr().err - ) +# def test_update_supported_python_versions_not_matching(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', +# ] +# """)) +# _what = update_supported_python_versions(str(filename), +# v(['3.7', '3.8'])) +# assert _what is None +# assert ( +# 'The value passed to classifiers is not a list' +# in capsys.readouterr().err +# ) 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', - classifiers=[ - 'Programming Language :: Python :: 3.6', - ] + 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']) @@ -131,17 +112,17 @@ 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', + name='foo' """)) assert get_python_requires(str(pyproject_toml)) is None - assert capsys.readouterr().err == '' + assert capsys.readouterr().err.strip() == 'The value passed to 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', + name='foo' [tool.poetry.dependencies] python = [">=3.6"] """)) @@ -157,43 +138,43 @@ def test_update_python_requires(tmp_path, fix_max_python_3_version): filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ [tool.poetry] - name='foo', + 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 "".join(result) == textwrap.dedent("""\ + assert "\n".join(result) == textwrap.dedent("""\ [tool.poetry] - name='foo', + 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 "".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 "".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', + name='foo' """)) fp.name = "pyproject.toml" result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) @@ -201,89 +182,87 @@ def test_update_python_requires_when_missing(capsys): 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 "".join(result) == textwrap.dedent("""\ - [tool.poetry] - name='foo', - [tool.poetry.dependencies] - python = ">=2.7,!=3.0.*,!=3.1.*" - """) - - -def test_update_python_requires_multiline(fix_max_python_3_version): - fix_max_python_3_version(2) - fp = StringIO(textwrap.dedent("""\ - [tool.poetry] - name='foo', - [tool.poetry.dependencies] - python = ', '.join([ - '>=2.7', - '!=3.0.*', - ]) - """)) - fp.name = "pyproject.toml" - result = update_python_requires(fp, v(['2.7', '3.2'])) - assert result is not None - assert "".join(result) == textwrap.dedent("""\ - [tool.poetry] - name='foo', - [tool.poetry.dependencies] - python = ', '.join([ - '>=2.7', - '!=3.0.*', - '!=3.1.*', - ]) - """) - - -def test_update_python_requires_multiline_variations(fix_max_python_3_version): - fix_max_python_3_version(2) - fp = StringIO(textwrap.dedent("""\ - [tool.poetry] - name='foo', - [tool.poetry.dependencies] - python = ",".join([ - ">=2.7", - "!=3.0.*", - ]) - """)) - fp.name = "pyproject.toml" - result = update_python_requires(fp, v(['2.7', '3.2'])) - assert result is not None - assert "".join(result) == textwrap.dedent("""\ - [tool.poetry] - name='foo', - [tool.poetry.dependencies] - python = ",".join([ - ">=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 = ', '.join([ - '>=2.7', - '!=3.0.*']) - """)) - fp.name = "pyproject.toml" - result = update_python_requires(fp, v(['2.7', '3.2'])) - assert result == fp.getvalue().splitlines(True) - assert ( - "Did not understand python_requires formatting in python dependency" - in 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 "".join(result) == textwrap.dedent("""\ +# [tool.poetry] +# name='foo' +# [tool.poetry.dependencies] +# python = ">=2.7,!=3.0.*,!=3.1.*" +# """) + +# +# def test_update_python_requires_multiline(fix_max_python_3_version): +# fix_max_python_3_version(2) +# fp = StringIO(textwrap.dedent("""\ +# [tool.poetry] +# name='foo' +# [tool.poetry.dependencies] +# python = ', '.join([ +# '>=2.7', +# '!=3.0.*', +# ]) +# """)) +# fp.name = "pyproject.toml" +# result = update_python_requires(fp, v(['2.7', '3.2'])) +# assert result is not None +# assert "".join(result) == textwrap.dedent("""\ +# [tool.poetry] +# name='foo' +# [tool.poetry.dependencies] +# python = ', '.join([ +# '>=2.7', +# '!=3.0.*', +# '!=3.1.*', +# ]) +# """) + + +# def test_update_python_requires_multiline_variations(fix_max_python_3_version): +# fix_max_python_3_version(2) +# fp = StringIO(textwrap.dedent("""\ +# [tool.poetry] +# name='foo' +# [tool.poetry.dependencies] +# python = ",".join([ +# ">=2.7", +# "!=3.0.*", +# ]) +# """)) +# fp.name = "pyproject.toml" +# result = update_python_requires(fp, v(['2.7', '3.2'])) +# assert result is not None +# assert "".join(result) == textwrap.dedent("""\ +# [tool.poetry] +# name='foo' +# [tool.poetry.dependencies] +# python = ",".join([ +# ">=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 == fp.getvalue().splitlines(True) +# assert ( +# "Did not understand python_requires formatting in python dependency" +# in capsys.readouterr().err +# ) From ac1b95d7e695476bb8745c4c66df6d811983eb5a Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 18:04:09 +0100 Subject: [PATCH 15/39] manage different tools that use pyproject.toml setuptools and flit are not implemented because struct not known --- .../sources/poetry_pyproject.py | 199 +++++++++++++++--- tests/sources/test_poetry_pyproject.py | 112 ++++++++++ 2 files changed, 276 insertions(+), 35 deletions(-) diff --git a/src/check_python_versions/sources/poetry_pyproject.py b/src/check_python_versions/sources/poetry_pyproject.py index a20202a..b4ddf60 100644 --- a/src/check_python_versions/sources/poetry_pyproject.py +++ b/src/check_python_versions/sources/poetry_pyproject.py @@ -13,6 +13,7 @@ from tomlkit import dumps from tomlkit import parse, load +from tomlkit import TOMLDocument from typing import ( List, @@ -38,8 +39,27 @@ PYPROJECT_TOML = 'pyproject.toml' +TOML_CLASSIFIERS_KWD = 'classifiers' +TOML_DEPENDENCIES_KWD = 'dependencies' +TOML_PYTHON_KWD = 'python' -def _load_toml(filename: FileOrFilename): +# poetry TOML keywords +TOML_TOOL_KWD = 'tool' +TOML_POETRY_KWD = 'poetry' +TOML_BUILD_SYSTEM_KWD = 'build-system' +TOML_BUILD_BACKEND_KWD = 'build-backend' +TOML_REQUIRES_KWD = 'requires' + +# setuptools TOML keywords +TOML_PROJECT_KWD = 'project' +TOML_SETUPTOOLS_KWD = 'setuptools' + +# flit TOML keywords +TOML_FLIT_KWD = 'flit' + + +def load_toml(filename: FileOrFilename) -> TOMLDocument: + """Utility method that returns a TOMLDocument.""" table = {} # tomlkit has two different API to load from file name or file object if isinstance(filename, str): @@ -54,39 +74,119 @@ def get_toml_content( filename: FileOrFilename = PYPROJECT_TOML ) -> FileLines: """Utility method to see if TOML library keeps style and comments.""" - table = _load_toml(filename) + table = load_toml(filename) return dumps(table).split('\n') +def is_poetry_toml( + table: TOMLDocument +) -> bool: + """Utility method to know if pyproject.toml is for poetry.""" + _ret = False + + if TOML_TOOL_KWD in table: + if TOML_POETRY_KWD in table[TOML_TOOL_KWD]: + _ret = True + if TOML_BUILD_SYSTEM_KWD in table: + if TOML_BUILD_BACKEND_KWD in table[TOML_BUILD_SYSTEM_KWD]: + if TOML_POETRY_KWD in table[TOML_BUILD_SYSTEM_KWD][TOML_BUILD_BACKEND_KWD]: + _ret = True + if TOML_REQUIRES_KWD in table[TOML_BUILD_SYSTEM_KWD]: + if list(filter(lambda x: TOML_POETRY_KWD in x, table[TOML_BUILD_SYSTEM_KWD][TOML_REQUIRES_KWD])): + _ret = True + return _ret + + +def is_setuptools_toml( + table: TOMLDocument +) -> bool: + """Utility method to know if pyproject.toml is for setuptool.""" + _ret = False + if TOML_TOOL_KWD in table: + if TOML_SETUPTOOLS_KWD in table[TOML_TOOL_KWD]: + _ret = True + if TOML_BUILD_SYSTEM_KWD in table: + if TOML_BUILD_BACKEND_KWD in table[TOML_BUILD_SYSTEM_KWD]: + if TOML_SETUPTOOLS_KWD in table[TOML_BUILD_SYSTEM_KWD][TOML_BUILD_BACKEND_KWD]: + _ret = True + if TOML_REQUIRES_KWD in table[TOML_BUILD_SYSTEM_KWD]: + if list(filter(lambda x: TOML_SETUPTOOLS_KWD in x, table[TOML_BUILD_SYSTEM_KWD][TOML_REQUIRES_KWD])): + _ret = True + return _ret + + +def is_flit_toml( + table: TOMLDocument +) -> bool: + """Utility method to know if pyproject.toml is for flit.""" + _ret = False + if TOML_TOOL_KWD in table: + if TOML_FLIT_KWD in table[TOML_TOOL_KWD]: + _ret = True + if TOML_BUILD_SYSTEM_KWD in table: + if TOML_BUILD_BACKEND_KWD in table[TOML_BUILD_SYSTEM_KWD]: + if TOML_FLIT_KWD in table[TOML_BUILD_SYSTEM_KWD][TOML_BUILD_BACKEND_KWD]: + _ret = True + if TOML_REQUIRES_KWD in table[TOML_BUILD_SYSTEM_KWD]: + if list(filter(lambda x: TOML_FLIT_KWD in x, table[TOML_BUILD_SYSTEM_KWD][TOML_REQUIRES_KWD])): + _ret = True + return _ret + + +def _get_poetry_classifiers(table: TOMLDocument) -> List[str]: + if TOML_TOOL_KWD not in table: + return [] + if TOML_POETRY_KWD not in table[TOML_TOOL_KWD]: + return [] + if TOML_CLASSIFIERS_KWD not in table[TOML_TOOL_KWD][TOML_POETRY_KWD]: + return [] + return table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_CLASSIFIERS_KWD] + + def _get_pyproject_toml_classifiers( filename: FileOrFilename = PYPROJECT_TOML ) -> List[str]: - table = _load_toml(filename) + _classifiers = [] + table = load_toml(filename) + if is_poetry_toml(table): + _classifiers = _get_poetry_classifiers(table) + + # missing implementation + # if is_setuptools_toml(table): + # _classifiers = _get_setuptools_classifiers(table) + # if is_flit_toml(table): + # _classifiers = _get_flit_classifiers(table) - if 'tool' not in table: + return _classifiers + + +def _get_poetry_python_requires(table: TOMLDocument) -> List[str]: + if TOML_TOOL_KWD not in table: return [] - if 'poetry' not in table['tool']: + if TOML_POETRY_KWD not in table[TOML_TOOL_KWD]: return [] - if 'classifiers' not in table['tool']['poetry']: + if TOML_DEPENDENCIES_KWD not in table[TOML_TOOL_KWD][TOML_POETRY_KWD]: return [] - - return table['tool']['poetry']['classifiers'] + if TOML_PYTHON_KWD not in table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_DEPENDENCIES_KWD]: + return [] + return table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_DEPENDENCIES_KWD][TOML_PYTHON_KWD] def _get_pyproject_toml_python_requires( filename: FileOrFilename = PYPROJECT_TOML ) -> List[str]: - table = _load_toml(filename) + _python_requires = [] + table = load_toml(filename) + if is_poetry_toml(table): + _python_requires = _get_poetry_python_requires(table) - if 'tool' not in table: - return [] - if 'poetry' not in table['tool']: - return [] - if 'dependencies' not in table['tool']['poetry']: - return [] - if 'python' not in table['tool']['poetry']['dependencies']: - return [] - return table['tool']['poetry']['dependencies']['python'] + # missing implementation + # if is_setuptools_toml(table): + # _classifiers = _get_setuptools_python_requires(table) + # if is_flit_toml(table): + # _classifiers = _get_flit_python_requires(table) + + return _python_requires def get_supported_python_versions( @@ -165,19 +265,47 @@ def update_python_requires( return _update_pyproject_toml_python_requires(filename, new_python_requires) +def _set_poetry_classifiers( + table: TOMLDocument, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + if TOML_TOOL_KWD not in table: + return [] + if TOML_POETRY_KWD not in table[TOML_TOOL_KWD]: + return [] + table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_CLASSIFIERS_KWD] = new_value + return dumps(table).split('\n') + + def _update_pyproject_toml_classifiers( filename: FileOrFilename, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - table = _load_toml(filename) + _updated_table = [] + table = load_toml(filename) + if is_poetry_toml(table): + _updated_table = _set_poetry_classifiers(table, new_value) - if 'tool' not in table: - return [] - if 'poetry' not in table['tool']: - return [] + # missing implementation + # if is_setuptools_toml(table): + # _updated_table = _set_setuptools_classifiers(table) + # if is_flit_toml(table): + # _updated_table = _set_flit_classifiers(table) - table['tool']['poetry']['classifiers'] = new_value + return _updated_table + +def _set_poetry_python_requires( + table: TOMLDocument, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + if TOML_TOOL_KWD not in table: + return [] + if TOML_POETRY_KWD not in table[TOML_TOOL_KWD]: + return [] + if TOML_DEPENDENCIES_KWD not in table[TOML_TOOL_KWD][TOML_POETRY_KWD]: + return [] + table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_DEPENDENCIES_KWD][TOML_PYTHON_KWD] = new_value return dumps(table).split('\n') @@ -185,17 +313,18 @@ def _update_pyproject_toml_python_requires( filename: FileOrFilename, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - table = _load_toml(filename) - - if 'tool' not in table: - return [] - if 'poetry' not in table['tool']: - return [] - if 'dependencies' not in table['tool']['poetry']: - return [] - - table['tool']['poetry']['dependencies']['python'] = new_value - return dumps(table).split('\n') + _updated_table = [] + table = load_toml(filename) + if is_poetry_toml(table): + _updated_table = _set_poetry_python_requires(table, new_value) + + # missing implementation + # if is_setuptools_toml(table): + # _updated_table = _set_setuptools_python_requires(table) + # if is_flit_toml(table): + # _updated_table = _set_flit_python_requires(table) + + return _updated_table PoetryPyProject = Source( diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py index 5bf4f2a..8c3fc2a 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_poetry_pyproject.py @@ -8,6 +8,10 @@ get_python_requires, get_supported_python_versions, get_toml_content, + is_flit_toml, + is_poetry_toml, + is_setuptools_toml, + load_toml, update_python_requires, update_supported_python_versions, ) @@ -266,3 +270,111 @@ def test_update_python_requires_when_missing(capsys): # "Did not understand python_requires formatting in python dependency" # in capsys.readouterr().err # ) + + +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) + + +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) + + +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) From dad449281e9bd6340c251ac2d41fda7dc6f3df40 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 18:05:09 +0100 Subject: [PATCH 16/39] renamed module to generic pyproject.py --- src/check_python_versions/sources/all.py | 2 +- .../sources/{poetry_pyproject.py => pyproject.py} | 0 tests/sources/test_poetry_pyproject.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/check_python_versions/sources/{poetry_pyproject.py => pyproject.py} (100%) diff --git a/src/check_python_versions/sources/all.py b/src/check_python_versions/sources/all.py index 442ec63..8d779f0 100644 --- a/src/check_python_versions/sources/all.py +++ b/src/check_python_versions/sources/all.py @@ -1,7 +1,7 @@ from .appveyor import Appveyor from .github import GitHubActions from .manylinux import Manylinux -from .poetry_pyproject import PoetryPyProject, PoetryPyProjectPythonRequires +from .pyproject import PoetryPyProject, PoetryPyProjectPythonRequires from .setup_py import SetupClassifiers, SetupPythonRequires from .tox import Tox from .travis import Travis diff --git a/src/check_python_versions/sources/poetry_pyproject.py b/src/check_python_versions/sources/pyproject.py similarity index 100% rename from src/check_python_versions/sources/poetry_pyproject.py rename to src/check_python_versions/sources/pyproject.py diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py index 8c3fc2a..e59b9ae 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_poetry_pyproject.py @@ -4,7 +4,7 @@ import pytest -from check_python_versions.sources.poetry_pyproject import ( +from check_python_versions.sources.pyproject import ( get_python_requires, get_supported_python_versions, get_toml_content, From 8a515de6b8ba669c7f0491a81b76c3caf712508b Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Tue, 24 Jan 2023 18:05:52 +0100 Subject: [PATCH 17/39] renamed objects removing poetry reference --- src/check_python_versions/sources/all.py | 6 +++--- src/check_python_versions/sources/pyproject.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/check_python_versions/sources/all.py b/src/check_python_versions/sources/all.py index 8d779f0..b3dd9a2 100644 --- a/src/check_python_versions/sources/all.py +++ b/src/check_python_versions/sources/all.py @@ -1,7 +1,7 @@ from .appveyor import Appveyor from .github import GitHubActions from .manylinux import Manylinux -from .pyproject import PoetryPyProject, PoetryPyProjectPythonRequires +from .pyproject import PyProject, PyProjectPythonRequires from .setup_py import SetupClassifiers, SetupPythonRequires from .tox import Tox from .travis import Travis @@ -13,8 +13,8 @@ ALL_SOURCES = [ SetupClassifiers, SetupPythonRequires, - PoetryPyProject, - PoetryPyProjectPythonRequires, + PyProject, + PyProjectPythonRequires, Tox, Travis, GitHubActions, diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index b4ddf60..cb1f425 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -327,7 +327,7 @@ def _update_pyproject_toml_python_requires( return _updated_table -PoetryPyProject = Source( +PyProject = Source( title=PYPROJECT_TOML, filename=PYPROJECT_TOML, extract=get_supported_python_versions, @@ -336,7 +336,7 @@ def _update_pyproject_toml_python_requires( has_upper_bound=True, ) -PoetryPyProjectPythonRequires = Source( +PyProjectPythonRequires = Source( title='- python_requires', filename=PYPROJECT_TOML, extract=get_python_requires, From 7ea9050a346ac936cecc1504c4803b282a67c8f0 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 09:48:58 +0100 Subject: [PATCH 18/39] tool.setuptools is optional table --- src/check_python_versions/sources/pyproject.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index cb1f425..0a35d25 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -102,9 +102,6 @@ def is_setuptools_toml( ) -> bool: """Utility method to know if pyproject.toml is for setuptool.""" _ret = False - if TOML_TOOL_KWD in table: - if TOML_SETUPTOOLS_KWD in table[TOML_TOOL_KWD]: - _ret = True if TOML_BUILD_SYSTEM_KWD in table: if TOML_BUILD_BACKEND_KWD in table[TOML_BUILD_SYSTEM_KWD]: if TOML_SETUPTOOLS_KWD in table[TOML_BUILD_SYSTEM_KWD][TOML_BUILD_BACKEND_KWD]: @@ -112,6 +109,13 @@ def is_setuptools_toml( if TOML_REQUIRES_KWD in table[TOML_BUILD_SYSTEM_KWD]: if list(filter(lambda x: TOML_SETUPTOOLS_KWD in x, table[TOML_BUILD_SYSTEM_KWD][TOML_REQUIRES_KWD])): _ret = True + + # from https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html#setuptools-specific-configuration + # "[tool.setuptools] table is still in beta" + # "These configurations are completely optional and probably can be skipped when creating simple packages" + if TOML_TOOL_KWD in table: + if TOML_SETUPTOOLS_KWD in table[TOML_TOOL_KWD]: + _ret = True return _ret From 12c5265fdbffcd5f795f0b565fc19f64ab3d8b73 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 09:49:41 +0100 Subject: [PATCH 19/39] removed wrong test --- tests/sources/test_poetry_pyproject.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py index e59b9ae..8a14d86 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_poetry_pyproject.py @@ -77,25 +77,6 @@ def test_get_supported_python_versions_string(tmp_path, capsys): ) -# def test_update_supported_python_versions_not_matching(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', -# ] -# """)) -# _what = update_supported_python_versions(str(filename), -# v(['3.7', '3.8'])) -# assert _what is None -# assert ( -# 'The value passed to classifiers is not a list' -# in capsys.readouterr().err -# ) - - def test_get_python_requires(tmp_path, fix_max_python_3_version): pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text(textwrap.dedent("""\ From ed48eda61ee2e436bd5055763dcc38000954c45b Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 09:50:04 +0100 Subject: [PATCH 20/39] same test name as setup.py --- tests/sources/test_poetry_pyproject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py index 8a14d86..44b07ba 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_poetry_pyproject.py @@ -60,7 +60,7 @@ def test_get_supported_python_versions_keep_comments(tmp_path): ''] -def test_get_supported_python_versions_string(tmp_path, capsys): +def test_update_supported_python_versions_not_a_list(tmp_path, capsys): filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ [tool.poetry] From d748483abd5bc42bdd9f6f5a36221833390013b4 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 09:51:43 +0100 Subject: [PATCH 21/39] unsupported dynamic format on toml file --- tests/sources/test_poetry_pyproject.py | 51 -------------------------- 1 file changed, 51 deletions(-) diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py index 44b07ba..b1d2ac0 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_poetry_pyproject.py @@ -184,57 +184,6 @@ def test_update_python_requires_when_missing(capsys): # python = ">=2.7,!=3.0.*,!=3.1.*" # """) -# -# def test_update_python_requires_multiline(fix_max_python_3_version): -# fix_max_python_3_version(2) -# fp = StringIO(textwrap.dedent("""\ -# [tool.poetry] -# name='foo' -# [tool.poetry.dependencies] -# python = ', '.join([ -# '>=2.7', -# '!=3.0.*', -# ]) -# """)) -# fp.name = "pyproject.toml" -# result = update_python_requires(fp, v(['2.7', '3.2'])) -# assert result is not None -# assert "".join(result) == textwrap.dedent("""\ -# [tool.poetry] -# name='foo' -# [tool.poetry.dependencies] -# python = ', '.join([ -# '>=2.7', -# '!=3.0.*', -# '!=3.1.*', -# ]) -# """) - - -# def test_update_python_requires_multiline_variations(fix_max_python_3_version): -# fix_max_python_3_version(2) -# fp = StringIO(textwrap.dedent("""\ -# [tool.poetry] -# name='foo' -# [tool.poetry.dependencies] -# python = ",".join([ -# ">=2.7", -# "!=3.0.*", -# ]) -# """)) -# fp.name = "pyproject.toml" -# result = update_python_requires(fp, v(['2.7', '3.2'])) -# assert result is not None -# assert "".join(result) == textwrap.dedent("""\ -# [tool.poetry] -# name='foo' -# [tool.poetry.dependencies] -# python = ",".join([ -# ">=2.7", -# "!=3.0.*", -# "!=3.1.*", -# ]) -# """) # def test_update_python_requires_multiline_error(capsys): From 583c3662284986b83aca436c4f8bff40ef0f543b Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 09:58:01 +0100 Subject: [PATCH 22/39] manage StringIO input --- src/check_python_versions/sources/pyproject.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index 0a35d25..489b052 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -11,6 +11,8 @@ check-python-versions supports both. """ +from io import StringIO + from tomlkit import dumps from tomlkit import parse, load from tomlkit import TOMLDocument @@ -62,7 +64,7 @@ def load_toml(filename: FileOrFilename) -> TOMLDocument: """Utility method that returns a TOMLDocument.""" table = {} # tomlkit has two different API to load from file name or file object - if isinstance(filename, str): + if isinstance(filename, str) or isinstance(filename, StringIO): with open_file(filename) as fp: table = load(fp) if isinstance(filename, TextIO): From b10da173543ff0758efb440391036d1ae2ea5c52 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 09:58:12 +0100 Subject: [PATCH 23/39] fixed latest tests --- tests/sources/test_poetry_pyproject.py | 100 ++++++++++++------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py index b1d2ac0..08547cf 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_poetry_pyproject.py @@ -137,23 +137,23 @@ def test_update_python_requires(tmp_path, fix_max_python_3_version): """) -# 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 "".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): @@ -167,39 +167,39 @@ def test_update_python_requires_when_missing(capsys): 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 "".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 == fp.getvalue().splitlines(True) -# assert ( -# "Did not understand python_requires formatting in python dependency" -# in 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(tmp_path): From 834950459750193cad550b8c200706e1993d3519 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 10:27:20 +0100 Subject: [PATCH 24/39] setuptools pyproject test and implementation --- .../sources/pyproject.py | 59 +++- tests/sources/test_poetry_pyproject.py | 37 --- tests/sources/test_setuptools_pyproject.py | 275 ++++++++++++++++++ 3 files changed, 325 insertions(+), 46 deletions(-) create mode 100644 tests/sources/test_setuptools_pyproject.py diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index 489b052..794dcab 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -55,6 +55,7 @@ # setuptools TOML keywords TOML_PROJECT_KWD = 'project' TOML_SETUPTOOLS_KWD = 'setuptools' +TOML_SETUPTOOLS_PYTHON_REQUIRES = 'requires-python' # flit TOML keywords TOML_FLIT_KWD = 'flit' @@ -149,6 +150,14 @@ def _get_poetry_classifiers(table: TOMLDocument) -> List[str]: return table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_CLASSIFIERS_KWD] +def _get_setuptools_classifiers(table: TOMLDocument) -> List[str]: + if TOML_PROJECT_KWD not in table: + return [] + if TOML_CLASSIFIERS_KWD not in table[TOML_PROJECT_KWD]: + return [] + return table[TOML_PROJECT_KWD][TOML_CLASSIFIERS_KWD] + + def _get_pyproject_toml_classifiers( filename: FileOrFilename = PYPROJECT_TOML ) -> List[str]: @@ -156,10 +165,10 @@ def _get_pyproject_toml_classifiers( table = load_toml(filename) if is_poetry_toml(table): _classifiers = _get_poetry_classifiers(table) + if is_setuptools_toml(table): + _classifiers = _get_setuptools_classifiers(table) # missing implementation - # if is_setuptools_toml(table): - # _classifiers = _get_setuptools_classifiers(table) # if is_flit_toml(table): # _classifiers = _get_flit_classifiers(table) @@ -178,6 +187,14 @@ def _get_poetry_python_requires(table: TOMLDocument) -> List[str]: return table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_DEPENDENCIES_KWD][TOML_PYTHON_KWD] +def _get_setuptools_python_requires(table: TOMLDocument) -> List[str]: + if TOML_PROJECT_KWD not in table: + return [] + if TOML_SETUPTOOLS_PYTHON_REQUIRES not in table[TOML_PROJECT_KWD]: + return [] + return table[TOML_PROJECT_KWD][TOML_SETUPTOOLS_PYTHON_REQUIRES] + + def _get_pyproject_toml_python_requires( filename: FileOrFilename = PYPROJECT_TOML ) -> List[str]: @@ -185,12 +202,12 @@ def _get_pyproject_toml_python_requires( table = load_toml(filename) if is_poetry_toml(table): _python_requires = _get_poetry_python_requires(table) + if is_setuptools_toml(table): + _python_requires = _get_setuptools_python_requires(table) # missing implementation - # if is_setuptools_toml(table): - # _classifiers = _get_setuptools_python_requires(table) # if is_flit_toml(table): - # _classifiers = _get_flit_python_requires(table) + # _python_requires = _get_flit_python_requires(table) return _python_requires @@ -283,6 +300,18 @@ def _set_poetry_classifiers( return dumps(table).split('\n') +def _set_setuptools_classifiers( + table: TOMLDocument, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + if TOML_PROJECT_KWD not in table: + return [] + if TOML_CLASSIFIERS_KWD not in table[TOML_PROJECT_KWD]: + return [] + table[TOML_PROJECT_KWD][TOML_CLASSIFIERS_KWD] = new_value + return dumps(table).split('\n') + + def _update_pyproject_toml_classifiers( filename: FileOrFilename, new_value: Union[str, List[str]], @@ -291,10 +320,10 @@ def _update_pyproject_toml_classifiers( table = load_toml(filename) if is_poetry_toml(table): _updated_table = _set_poetry_classifiers(table, new_value) + if is_setuptools_toml(table): + _updated_table = _set_setuptools_classifiers(table, new_value) # missing implementation - # if is_setuptools_toml(table): - # _updated_table = _set_setuptools_classifiers(table) # if is_flit_toml(table): # _updated_table = _set_flit_classifiers(table) @@ -315,6 +344,18 @@ def _set_poetry_python_requires( return dumps(table).split('\n') +def _set_setuptools_python_requires( + table: TOMLDocument, + new_value: Union[str, List[str]], +) -> Optional[FileLines]: + if TOML_PROJECT_KWD not in table: + return [] + if TOML_SETUPTOOLS_PYTHON_REQUIRES not in table[TOML_PROJECT_KWD]: + return [] + table[TOML_PROJECT_KWD][TOML_SETUPTOOLS_PYTHON_REQUIRES] = new_value + return dumps(table).split('\n') + + def _update_pyproject_toml_python_requires( filename: FileOrFilename, new_value: Union[str, List[str]], @@ -323,10 +364,10 @@ def _update_pyproject_toml_python_requires( table = load_toml(filename) if is_poetry_toml(table): _updated_table = _set_poetry_python_requires(table, new_value) + if is_setuptools_toml(table): + _updated_table = _set_setuptools_python_requires(table, new_value) # missing implementation - # if is_setuptools_toml(table): - # _updated_table = _set_setuptools_python_requires(table) # if is_flit_toml(table): # _updated_table = _set_flit_python_requires(table) diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_poetry_pyproject.py index 08547cf..abebba2 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_poetry_pyproject.py @@ -13,7 +13,6 @@ is_setuptools_toml, load_toml, update_python_requires, - update_supported_python_versions, ) from check_python_versions.versions import Version @@ -238,42 +237,6 @@ def test_poetry_toml_from_build_requires(tmp_path): assert not is_flit_toml(_table) -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) - - def test_flit_toml_from_tools(tmp_path): filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ diff --git a/tests/sources/test_setuptools_pyproject.py b/tests/sources/test_setuptools_pyproject.py new file mode 100644 index 0000000..29e287b --- /dev/null +++ b/tests/sources/test_setuptools_pyproject.py @@ -0,0 +1,275 @@ +import textwrap +from io import StringIO +from typing import List + +import pytest + +from check_python_versions.sources.pyproject import ( + get_python_requires, + get_supported_python_versions, + get_toml_content, + is_flit_toml, + is_poetry_toml, + is_setuptools_toml, + load_toml, + update_python_requires, +) +from check_python_versions.versions import Version + + +def v(versions: List[str]) -> List[Version]: + return [Version.from_string(v) for v in versions] + + +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_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 passed to classifiers is not a list" + in capsys.readouterr().err + ) + + +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 passed to 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 passed to 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) From c7e1623d9282ec7c087b597cd7d9758710530765 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 10:39:00 +0100 Subject: [PATCH 25/39] renamed files to have all pyproject tests with same prefix --- ..._pyproject.py => test_pyproject_poetry.py} | 36 ------------------- ...roject.py => test_pyproject_setuptools.py} | 0 2 files changed, 36 deletions(-) rename tests/sources/{test_poetry_pyproject.py => test_pyproject_poetry.py} (87%) rename tests/sources/{test_setuptools_pyproject.py => test_pyproject_setuptools.py} (100%) diff --git a/tests/sources/test_poetry_pyproject.py b/tests/sources/test_pyproject_poetry.py similarity index 87% rename from tests/sources/test_poetry_pyproject.py rename to tests/sources/test_pyproject_poetry.py index abebba2..5735696 100644 --- a/tests/sources/test_poetry_pyproject.py +++ b/tests/sources/test_pyproject_poetry.py @@ -235,39 +235,3 @@ def test_poetry_toml_from_build_requires(tmp_path): assert is_poetry_toml(_table) assert not is_setuptools_toml(_table) assert not is_flit_toml(_table) - - -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_setuptools_pyproject.py b/tests/sources/test_pyproject_setuptools.py similarity index 100% rename from tests/sources/test_setuptools_pyproject.py rename to tests/sources/test_pyproject_setuptools.py From 82c67eaf7a83afc28f294ee37460e83f2816383a Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 10:39:49 +0100 Subject: [PATCH 26/39] added flit implementation and tests --- .../sources/pyproject.py | 50 ++-- tests/sources/test_pyproject_flit.py | 275 ++++++++++++++++++ 2 files changed, 292 insertions(+), 33 deletions(-) create mode 100644 tests/sources/test_pyproject_flit.py diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index 794dcab..a04cd07 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -44,6 +44,7 @@ TOML_CLASSIFIERS_KWD = 'classifiers' TOML_DEPENDENCIES_KWD = 'dependencies' TOML_PYTHON_KWD = 'python' +TOML_PYTHON_REQUIRES_KWD = 'requires-python' # poetry TOML keywords TOML_TOOL_KWD = 'tool' @@ -55,7 +56,6 @@ # setuptools TOML keywords TOML_PROJECT_KWD = 'project' TOML_SETUPTOOLS_KWD = 'setuptools' -TOML_SETUPTOOLS_PYTHON_REQUIRES = 'requires-python' # flit TOML keywords TOML_FLIT_KWD = 'flit' @@ -150,7 +150,7 @@ def _get_poetry_classifiers(table: TOMLDocument) -> List[str]: return table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_CLASSIFIERS_KWD] -def _get_setuptools_classifiers(table: TOMLDocument) -> List[str]: +def _get_setuptools_flit_classifiers(table: TOMLDocument) -> List[str]: if TOML_PROJECT_KWD not in table: return [] if TOML_CLASSIFIERS_KWD not in table[TOML_PROJECT_KWD]: @@ -165,12 +165,8 @@ def _get_pyproject_toml_classifiers( table = load_toml(filename) if is_poetry_toml(table): _classifiers = _get_poetry_classifiers(table) - if is_setuptools_toml(table): - _classifiers = _get_setuptools_classifiers(table) - - # missing implementation - # if is_flit_toml(table): - # _classifiers = _get_flit_classifiers(table) + if is_setuptools_toml(table) or is_flit_toml(table): + _classifiers = _get_setuptools_flit_classifiers(table) return _classifiers @@ -187,12 +183,12 @@ def _get_poetry_python_requires(table: TOMLDocument) -> List[str]: return table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_DEPENDENCIES_KWD][TOML_PYTHON_KWD] -def _get_setuptools_python_requires(table: TOMLDocument) -> List[str]: +def _get_setuptools_flit_python_requires(table: TOMLDocument) -> List[str]: if TOML_PROJECT_KWD not in table: return [] - if TOML_SETUPTOOLS_PYTHON_REQUIRES not in table[TOML_PROJECT_KWD]: + if TOML_PYTHON_REQUIRES_KWD not in table[TOML_PROJECT_KWD]: return [] - return table[TOML_PROJECT_KWD][TOML_SETUPTOOLS_PYTHON_REQUIRES] + return table[TOML_PROJECT_KWD][TOML_PYTHON_REQUIRES_KWD] def _get_pyproject_toml_python_requires( @@ -202,12 +198,8 @@ def _get_pyproject_toml_python_requires( table = load_toml(filename) if is_poetry_toml(table): _python_requires = _get_poetry_python_requires(table) - if is_setuptools_toml(table): - _python_requires = _get_setuptools_python_requires(table) - - # missing implementation - # if is_flit_toml(table): - # _python_requires = _get_flit_python_requires(table) + if is_setuptools_toml(table) or is_flit_toml(table): + _python_requires = _get_setuptools_flit_python_requires(table) return _python_requires @@ -300,7 +292,7 @@ def _set_poetry_classifiers( return dumps(table).split('\n') -def _set_setuptools_classifiers( +def _set_setuptools_flit_classifiers( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: @@ -320,12 +312,8 @@ def _update_pyproject_toml_classifiers( table = load_toml(filename) if is_poetry_toml(table): _updated_table = _set_poetry_classifiers(table, new_value) - if is_setuptools_toml(table): - _updated_table = _set_setuptools_classifiers(table, new_value) - - # missing implementation - # if is_flit_toml(table): - # _updated_table = _set_flit_classifiers(table) + if is_setuptools_toml(table) or is_flit_toml(table): + _updated_table = _set_setuptools_flit_classifiers(table, new_value) return _updated_table @@ -344,15 +332,15 @@ def _set_poetry_python_requires( return dumps(table).split('\n') -def _set_setuptools_python_requires( +def _set_setuptools_flit_python_requires( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: if TOML_PROJECT_KWD not in table: return [] - if TOML_SETUPTOOLS_PYTHON_REQUIRES not in table[TOML_PROJECT_KWD]: + if TOML_PYTHON_REQUIRES_KWD not in table[TOML_PROJECT_KWD]: return [] - table[TOML_PROJECT_KWD][TOML_SETUPTOOLS_PYTHON_REQUIRES] = new_value + table[TOML_PROJECT_KWD][TOML_PYTHON_REQUIRES_KWD] = new_value return dumps(table).split('\n') @@ -364,12 +352,8 @@ def _update_pyproject_toml_python_requires( table = load_toml(filename) if is_poetry_toml(table): _updated_table = _set_poetry_python_requires(table, new_value) - if is_setuptools_toml(table): - _updated_table = _set_setuptools_python_requires(table, new_value) - - # missing implementation - # if is_flit_toml(table): - # _updated_table = _set_flit_python_requires(table) + if is_setuptools_toml(table) or is_flit_toml(table): + _updated_table = _set_setuptools_flit_python_requires(table, new_value) return _updated_table diff --git a/tests/sources/test_pyproject_flit.py b/tests/sources/test_pyproject_flit.py new file mode 100644 index 0000000..87b33bb --- /dev/null +++ b/tests/sources/test_pyproject_flit.py @@ -0,0 +1,275 @@ +import textwrap +from io import StringIO +from typing import List + +import pytest + +from check_python_versions.sources.pyproject import ( + get_python_requires, + get_supported_python_versions, + get_toml_content, + is_flit_toml, + is_poetry_toml, + is_setuptools_toml, + load_toml, + update_python_requires, +) +from check_python_versions.versions import Version + + +def v(versions: List[str]) -> List[Version]: + return [Version.from_string(v) for v in versions] + + +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 passed to classifiers is not a list" + in capsys.readouterr().err + ) + + +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 passed to 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 passed to 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) From 63e735a2a2100bd63443b8356d2b212acaf4b5e1 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 15:50:11 +0100 Subject: [PATCH 27/39] fix trailing whitespace --- tests/sources/test_pyproject_flit.py | 12 ++++++------ tests/sources/test_pyproject_setuptools.py | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/sources/test_pyproject_flit.py b/tests/sources/test_pyproject_flit.py index 87b33bb..233142b 100644 --- a/tests/sources/test_pyproject_flit.py +++ b/tests/sources/test_pyproject_flit.py @@ -96,7 +96,7 @@ def test_get_python_requires(tmp_path, fix_max_python_3_version): requires-python = ">=3.6" [build-system] requires = ["flit_core >=3.2,<4"] - build-backend = "flit_core.buildapi" + build-backend = "flit_core.buildapi" """)) fix_max_python_3_version(7) assert get_python_requires(str(pyproject_toml)) == v(['3.6', '3.7']) @@ -113,7 +113,7 @@ def test_get_python_requires_not_specified(tmp_path, capsys): name='foo' [build-system] requires = ["flit_core >=3.2,<4"] - build-backend = "flit_core.buildapi" + build-backend = "flit_core.buildapi" """)) assert get_python_requires(str(pyproject_toml)) is None assert capsys.readouterr().err.strip() == 'The value passed to python dependency is not a string' @@ -127,7 +127,7 @@ def test_get_python_requires_not_a_string(tmp_path, capsys): requires-python = [">=3.6"] [build-system] requires = ["flit_core >=3.2,<4"] - build-backend = "flit_core.buildapi" + build-backend = "flit_core.buildapi" """)) assert get_python_requires(str(pyproject_toml)) is None assert ( @@ -167,7 +167,7 @@ def test_update_python_requires_file_object(fix_max_python_3_version): requires-python = ">=3.4" [build-system] requires = ["flit_core >=3.2,<4"] - build-backend = "flit_core.buildapi" + build-backend = "flit_core.buildapi" """)) fp.name = "pyproject.toml" result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) @@ -178,7 +178,7 @@ def test_update_python_requires_file_object(fix_max_python_3_version): requires-python = ">=3.5" [build-system] requires = ["flit_core >=3.2,<4"] - build-backend = "flit_core.buildapi" + build-backend = "flit_core.buildapi" """) @@ -188,7 +188,7 @@ def test_update_python_requires_when_missing(capsys): name='foo' [build-system] requires = ["flit_core >=3.2,<4"] - build-backend = "flit_core.buildapi" + build-backend = "flit_core.buildapi" """)) fp.name = "pyproject.toml" result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) diff --git a/tests/sources/test_pyproject_setuptools.py b/tests/sources/test_pyproject_setuptools.py index 29e287b..d3db269 100644 --- a/tests/sources/test_pyproject_setuptools.py +++ b/tests/sources/test_pyproject_setuptools.py @@ -96,7 +96,7 @@ def test_get_python_requires(tmp_path, fix_max_python_3_version): requires-python = ">=3.6" [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """)) fix_max_python_3_version(7) assert get_python_requires(str(pyproject_toml)) == v(['3.6', '3.7']) @@ -113,7 +113,7 @@ def test_get_python_requires_not_specified(tmp_path, capsys): name='foo' [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """)) assert get_python_requires(str(pyproject_toml)) is None assert capsys.readouterr().err.strip() == 'The value passed to python dependency is not a string' @@ -127,7 +127,7 @@ def test_get_python_requires_not_a_string(tmp_path, capsys): requires-python = [">=3.6"] [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """)) assert get_python_requires(str(pyproject_toml)) is None assert ( @@ -145,7 +145,7 @@ def test_update_python_requires(tmp_path, fix_max_python_3_version): requires-python = ">=3.4" [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """)) result = update_python_requires(str(filename), v(['3.5', '3.6', '3.7'])) assert result is not None @@ -155,7 +155,7 @@ def test_update_python_requires(tmp_path, fix_max_python_3_version): requires-python = ">=3.5" [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """) @@ -167,7 +167,7 @@ def test_update_python_requires_file_object(fix_max_python_3_version): requires-python = ">=3.4" [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """)) fp.name = "pyproject.toml" result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) @@ -178,7 +178,7 @@ def test_update_python_requires_file_object(fix_max_python_3_version): requires-python = ">=3.5" [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """) @@ -188,7 +188,7 @@ def test_update_python_requires_when_missing(capsys): name='foo' [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """)) fp.name = "pyproject.toml" result = update_python_requires(fp, v(['3.5', '3.6', '3.7'])) @@ -204,7 +204,7 @@ def test_update_python_requires_preserves_style(fix_max_python_3_version): requires-python = ">=2.7,!=3.0.*" [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """)) fp.name = "pyproject.toml" result = update_python_requires(fp, v(['2.7', '3.2'])) @@ -214,7 +214,7 @@ def test_update_python_requires_preserves_style(fix_max_python_3_version): requires-python = ">=2.7,!=3.0.*,!=3.1.*" [build-system] requires = ["setuptools", "setuptools-scm"] - build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" """) @@ -243,7 +243,7 @@ def test_setuptools_toml_from_tools(tmp_path): filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ [tool.setuptools.packages] - name='foo' + name='foo' """)) _table = load_toml(str(filename)) assert is_setuptools_toml(_table) From 7ed0b9a5777ecbf6120d5c16f3f29779dd225a86 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 15:51:45 +0100 Subject: [PATCH 28/39] review comments --- .../sources/pyproject.py | 20 ++++++------------- tests/sources/test_pyproject_flit.py | 20 +++++++++++++++---- tests/sources/test_pyproject_poetry.py | 20 +++++++++++++++---- tests/sources/test_pyproject_setuptools.py | 14 ++++++++++++- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index a04cd07..37936b6 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -14,7 +14,7 @@ from io import StringIO from tomlkit import dumps -from tomlkit import parse, load +from tomlkit import load from tomlkit import TOMLDocument from typing import ( @@ -73,14 +73,6 @@ def load_toml(filename: FileOrFilename) -> TOMLDocument: return table -def get_toml_content( - filename: FileOrFilename = PYPROJECT_TOML -) -> FileLines: - """Utility method to see if TOML library keeps style and comments.""" - table = load_toml(filename) - return dumps(table).split('\n') - - def is_poetry_toml( table: TOMLDocument ) -> bool: @@ -216,8 +208,8 @@ def get_supported_python_versions( # versions in classifiers. return [] - if not isinstance(classifiers, (list, tuple)): - warn('The value passed to classifiers is not a list') + if not isinstance(classifiers, list): + warn('The value specified for classifiers is not a list') return [] return get_versions_from_classifiers(classifiers) @@ -231,7 +223,7 @@ def get_python_requires( if python_requires is None: return None if not isinstance(python_requires, str): - warn('The value passed to python dependency is not a string') + warn('The value specified for python dependency is not a string') return None return parse_python_requires(python_requires) @@ -247,8 +239,8 @@ def update_supported_python_versions( classifiers = _get_pyproject_toml_classifiers(filename) if classifiers is None: return None - if not isinstance(classifiers, (list, tuple)): - warn('The value passed to classifiers is not a list') + if not isinstance(classifiers, list): + warn('The value specified for classifiers is not a list') return None new_classifiers = update_classifiers(classifiers, new_versions) return _update_pyproject_toml_classifiers(filename, new_classifiers) diff --git a/tests/sources/test_pyproject_flit.py b/tests/sources/test_pyproject_flit.py index 233142b..fd7593c 100644 --- a/tests/sources/test_pyproject_flit.py +++ b/tests/sources/test_pyproject_flit.py @@ -1,5 +1,6 @@ import textwrap from io import StringIO +from tomlkit import dumps from typing import List import pytest @@ -7,13 +8,16 @@ from check_python_versions.sources.pyproject import ( get_python_requires, get_supported_python_versions, - get_toml_content, is_flit_toml, is_poetry_toml, is_setuptools_toml, load_toml, update_python_requires, ) +from check_python_versions.utils import ( + FileLines, + FileOrFilename, +) from check_python_versions.versions import Version @@ -21,6 +25,14 @@ 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("""\ @@ -83,7 +95,7 @@ def test_update_supported_python_versions_not_a_list(tmp_path, capsys): """)) assert get_supported_python_versions(str(filename)) == [] assert ( - "The value passed to classifiers is not a list" + "The value specified for classifiers is not an array" in capsys.readouterr().err ) @@ -116,7 +128,7 @@ def test_get_python_requires_not_specified(tmp_path, capsys): build-backend = "flit_core.buildapi" """)) assert get_python_requires(str(pyproject_toml)) is None - assert capsys.readouterr().err.strip() == 'The value passed to python dependency is not a string' + 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): @@ -131,7 +143,7 @@ def test_get_python_requires_not_a_string(tmp_path, capsys): """)) assert get_python_requires(str(pyproject_toml)) is None assert ( - 'The value passed to python dependency is not a string' + 'The value specified for python dependency is not a string' in capsys.readouterr().err ) diff --git a/tests/sources/test_pyproject_poetry.py b/tests/sources/test_pyproject_poetry.py index 5735696..06f19e4 100644 --- a/tests/sources/test_pyproject_poetry.py +++ b/tests/sources/test_pyproject_poetry.py @@ -1,5 +1,6 @@ import textwrap from io import StringIO +from tomlkit import dumps from typing import List import pytest @@ -7,13 +8,16 @@ from check_python_versions.sources.pyproject import ( get_python_requires, get_supported_python_versions, - get_toml_content, is_flit_toml, is_poetry_toml, is_setuptools_toml, load_toml, update_python_requires, ) +from check_python_versions.utils import ( + FileLines, + FileOrFilename, +) from check_python_versions.versions import Version @@ -21,6 +25,14 @@ 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("""\ @@ -71,7 +83,7 @@ def test_update_supported_python_versions_not_a_list(tmp_path, capsys): """)) assert get_supported_python_versions(str(filename)) == [] assert ( - "The value passed to classifiers is not a list" + "The value specified for classifiers is not an array" in capsys.readouterr().err ) @@ -99,7 +111,7 @@ def test_get_python_requires_not_specified(tmp_path, capsys): name='foo' """)) assert get_python_requires(str(pyproject_toml)) is None - assert capsys.readouterr().err.strip() == 'The value passed to python dependency is not a string' + 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): @@ -112,7 +124,7 @@ def test_get_python_requires_not_a_string(tmp_path, capsys): """)) assert get_python_requires(str(pyproject_toml)) is None assert ( - 'The value passed to python dependency is not a string' + 'The value specified for python dependency is not a string' in capsys.readouterr().err ) diff --git a/tests/sources/test_pyproject_setuptools.py b/tests/sources/test_pyproject_setuptools.py index d3db269..bd92d77 100644 --- a/tests/sources/test_pyproject_setuptools.py +++ b/tests/sources/test_pyproject_setuptools.py @@ -1,5 +1,6 @@ import textwrap from io import StringIO +from tomlkit import dumps from typing import List import pytest @@ -7,13 +8,16 @@ from check_python_versions.sources.pyproject import ( get_python_requires, get_supported_python_versions, - get_toml_content, is_flit_toml, is_poetry_toml, is_setuptools_toml, load_toml, update_python_requires, ) +from check_python_versions.utils import ( + FileLines, + FileOrFilename, +) from check_python_versions.versions import Version @@ -21,6 +25,14 @@ 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("""\ From 605fe9a26aa46017c4066c70c1a5c702bfcb1207 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 16:02:36 +0100 Subject: [PATCH 29/39] pass flake8 tests --- .../sources/pyproject.py | 189 ++++++++++-------- tests/sources/test_pyproject_flit.py | 36 ++-- tests/sources/test_pyproject_poetry.py | 30 +-- tests/sources/test_pyproject_setuptools.py | 42 ++-- tests/test_cli.py | 3 +- 5 files changed, 169 insertions(+), 131 deletions(-) diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index 37936b6..08cf8d1 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -25,7 +25,12 @@ cast, ) -from .setup_py import get_versions_from_classifiers, parse_python_requires, update_classifiers, compute_python_requires +from .setup_py import ( + get_versions_from_classifiers, + parse_python_requires, + update_classifiers, + compute_python_requires, +) from .base import Source from ..utils import ( FileLines, @@ -41,27 +46,29 @@ PYPROJECT_TOML = 'pyproject.toml' -TOML_CLASSIFIERS_KWD = 'classifiers' -TOML_DEPENDENCIES_KWD = 'dependencies' -TOML_PYTHON_KWD = 'python' -TOML_PYTHON_REQUIRES_KWD = 'requires-python' +CLASSIFIERS = 'classifiers' +DEPENDENCIES = 'dependencies' +PYTHON = 'python' +PYTHON_REQUIRES = 'requires-python' # poetry TOML keywords -TOML_TOOL_KWD = 'tool' -TOML_POETRY_KWD = 'poetry' -TOML_BUILD_SYSTEM_KWD = 'build-system' -TOML_BUILD_BACKEND_KWD = 'build-backend' -TOML_REQUIRES_KWD = 'requires' +TOOL = 'tool' +POETRY = 'poetry' +BUILD_SYSTEM = 'build-system' +BUILD_BACKEND = 'build-backend' +REQUIRES = 'requires' # setuptools TOML keywords -TOML_PROJECT_KWD = 'project' -TOML_SETUPTOOLS_KWD = 'setuptools' +PROJECT = 'project' +SETUPTOOLS = 'setuptools' # flit TOML keywords -TOML_FLIT_KWD = 'flit' +FLIT = 'flit' -def load_toml(filename: FileOrFilename) -> TOMLDocument: +def load_toml( + filename: FileOrFilename +) -> TOMLDocument: """Utility method that returns a TOMLDocument.""" table = {} # tomlkit has two different API to load from file name or file object @@ -79,15 +86,17 @@ def is_poetry_toml( """Utility method to know if pyproject.toml is for poetry.""" _ret = False - if TOML_TOOL_KWD in table: - if TOML_POETRY_KWD in table[TOML_TOOL_KWD]: + if TOOL in table: + if POETRY in table[TOOL]: _ret = True - if TOML_BUILD_SYSTEM_KWD in table: - if TOML_BUILD_BACKEND_KWD in table[TOML_BUILD_SYSTEM_KWD]: - if TOML_POETRY_KWD in table[TOML_BUILD_SYSTEM_KWD][TOML_BUILD_BACKEND_KWD]: + if BUILD_SYSTEM in table: + if BUILD_BACKEND in table[BUILD_SYSTEM]: + if POETRY in \ + table[BUILD_SYSTEM][BUILD_BACKEND]: _ret = True - if TOML_REQUIRES_KWD in table[TOML_BUILD_SYSTEM_KWD]: - if list(filter(lambda x: TOML_POETRY_KWD in x, table[TOML_BUILD_SYSTEM_KWD][TOML_REQUIRES_KWD])): + if REQUIRES in table[BUILD_SYSTEM]: + if list(filter(lambda x: POETRY in x, + table[BUILD_SYSTEM][REQUIRES])): _ret = True return _ret @@ -97,19 +106,21 @@ def is_setuptools_toml( ) -> bool: """Utility method to know if pyproject.toml is for setuptool.""" _ret = False - if TOML_BUILD_SYSTEM_KWD in table: - if TOML_BUILD_BACKEND_KWD in table[TOML_BUILD_SYSTEM_KWD]: - if TOML_SETUPTOOLS_KWD in table[TOML_BUILD_SYSTEM_KWD][TOML_BUILD_BACKEND_KWD]: + if BUILD_SYSTEM in table: + if BUILD_BACKEND in table[BUILD_SYSTEM]: + if SETUPTOOLS in \ + table[BUILD_SYSTEM][BUILD_BACKEND]: _ret = True - if TOML_REQUIRES_KWD in table[TOML_BUILD_SYSTEM_KWD]: - if list(filter(lambda x: TOML_SETUPTOOLS_KWD in x, table[TOML_BUILD_SYSTEM_KWD][TOML_REQUIRES_KWD])): + if REQUIRES in table[BUILD_SYSTEM]: + if list(filter(lambda x: SETUPTOOLS in x, + table[BUILD_SYSTEM][REQUIRES])): _ret = True - # from https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html#setuptools-specific-configuration # "[tool.setuptools] table is still in beta" - # "These configurations are completely optional and probably can be skipped when creating simple packages" - if TOML_TOOL_KWD in table: - if TOML_SETUPTOOLS_KWD in table[TOML_TOOL_KWD]: + # "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 @@ -119,35 +130,46 @@ def is_flit_toml( ) -> bool: """Utility method to know if pyproject.toml is for flit.""" _ret = False - if TOML_TOOL_KWD in table: - if TOML_FLIT_KWD in table[TOML_TOOL_KWD]: + if TOOL in table: + if FLIT in table[TOOL]: _ret = True - if TOML_BUILD_SYSTEM_KWD in table: - if TOML_BUILD_BACKEND_KWD in table[TOML_BUILD_SYSTEM_KWD]: - if TOML_FLIT_KWD in table[TOML_BUILD_SYSTEM_KWD][TOML_BUILD_BACKEND_KWD]: + if BUILD_SYSTEM in table: + if BUILD_BACKEND in \ + table[BUILD_SYSTEM]: + if FLIT in \ + table[BUILD_SYSTEM][BUILD_BACKEND]: _ret = True - if TOML_REQUIRES_KWD in table[TOML_BUILD_SYSTEM_KWD]: - if list(filter(lambda x: TOML_FLIT_KWD in x, table[TOML_BUILD_SYSTEM_KWD][TOML_REQUIRES_KWD])): + 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 TOML_TOOL_KWD not in table: +def _get_poetry_classifiers( + table: TOMLDocument +) -> List[str]: + if TOOL not in table: return [] - if TOML_POETRY_KWD not in table[TOML_TOOL_KWD]: + if POETRY not in \ + table[TOOL]: return [] - if TOML_CLASSIFIERS_KWD not in table[TOML_TOOL_KWD][TOML_POETRY_KWD]: + if CLASSIFIERS not in \ + table[TOOL][POETRY]: return [] - return table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_CLASSIFIERS_KWD] + return table[TOOL][POETRY][CLASSIFIERS] -def _get_setuptools_flit_classifiers(table: TOMLDocument) -> List[str]: - if TOML_PROJECT_KWD not in table: +def _get_setuptools_flit_classifiers( + table: TOMLDocument +) -> List[str]: + if PROJECT not in table: return [] - if TOML_CLASSIFIERS_KWD not in table[TOML_PROJECT_KWD]: + if CLASSIFIERS not in \ + table[PROJECT]: return [] - return table[TOML_PROJECT_KWD][TOML_CLASSIFIERS_KWD] + return table[PROJECT][CLASSIFIERS] def _get_pyproject_toml_classifiers( @@ -163,24 +185,31 @@ def _get_pyproject_toml_classifiers( return _classifiers -def _get_poetry_python_requires(table: TOMLDocument) -> List[str]: - if TOML_TOOL_KWD not in table: +def _get_poetry_python_requires( + table: TOMLDocument +) -> List[str]: + if TOOL not in table: return [] - if TOML_POETRY_KWD not in table[TOML_TOOL_KWD]: + if POETRY not in table[TOOL]: return [] - if TOML_DEPENDENCIES_KWD not in table[TOML_TOOL_KWD][TOML_POETRY_KWD]: + if DEPENDENCIES not in \ + table[TOOL][POETRY]: return [] - if TOML_PYTHON_KWD not in table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_DEPENDENCIES_KWD]: + if PYTHON not in \ + table[TOOL][POETRY][DEPENDENCIES]: return [] - return table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_DEPENDENCIES_KWD][TOML_PYTHON_KWD] + return table[TOOL][POETRY][DEPENDENCIES][PYTHON] -def _get_setuptools_flit_python_requires(table: TOMLDocument) -> List[str]: - if TOML_PROJECT_KWD not in table: +def _get_setuptools_flit_python_requires( + table: TOMLDocument +) -> List[str]: + if PROJECT not in table: return [] - if TOML_PYTHON_REQUIRES_KWD not in table[TOML_PROJECT_KWD]: + if PYTHON_REQUIRES not in \ + table[PROJECT]: return [] - return table[TOML_PROJECT_KWD][TOML_PYTHON_REQUIRES_KWD] + return table[PROJECT][PYTHON_REQUIRES] def _get_pyproject_toml_python_requires( @@ -199,13 +228,14 @@ def _get_pyproject_toml_python_requires( def get_supported_python_versions( filename: FileOrFilename = PYPROJECT_TOML ) -> SortedVersionList: - """Extract supported Python versions from classifiers in pyproject.toml .""" + """Extract supported Python versions from classifiers in pyproject.toml.""" classifiers = _get_pyproject_toml_classifiers(filename) if classifiers is None: - # Note: do not return None because pyproject.toml is not an optional source! - # We want errors to show up if pyproject.toml fails to declare Python - # versions in classifiers. + # Note: do not return None because pyproject.toml is + # not an optional source! + # We want errors to show up if pyproject.toml fails to + # declare Python versions in classifiers. return [] if not isinstance(classifiers, list): @@ -218,7 +248,8 @@ def get_supported_python_versions( def get_python_requires( pyproject_toml: FileOrFilename = PYPROJECT_TOML, ) -> Optional[SortedVersionList]: - """Extract supported Python versions from python_requires in pyproject.toml.""" + """Extract supported Python versions from python_requires in + pyproject.toml.""" python_requires = _get_pyproject_toml_python_requires(pyproject_toml) if python_requires is None: return None @@ -263,24 +294,24 @@ def update_python_requires( space = '' if '> ' in python_requires or '= ' in python_requires: space = ' ' - new_python_requires = compute_python_requires( + 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_toml_python_requires(filename, new_python_requires) + return _update_pyproject_python_requires(filename, new_requires) def _set_poetry_classifiers( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - if TOML_TOOL_KWD not in table: + if TOOL not in table: return [] - if TOML_POETRY_KWD not in table[TOML_TOOL_KWD]: + if POETRY not in table[TOOL]: return [] - table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_CLASSIFIERS_KWD] = new_value + table[TOOL][POETRY][CLASSIFIERS] = new_value return dumps(table).split('\n') @@ -288,11 +319,11 @@ def _set_setuptools_flit_classifiers( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - if TOML_PROJECT_KWD not in table: + if PROJECT not in table: return [] - if TOML_CLASSIFIERS_KWD not in table[TOML_PROJECT_KWD]: + if CLASSIFIERS not in table[PROJECT]: return [] - table[TOML_PROJECT_KWD][TOML_CLASSIFIERS_KWD] = new_value + table[PROJECT][CLASSIFIERS] = new_value return dumps(table).split('\n') @@ -314,13 +345,13 @@ def _set_poetry_python_requires( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - if TOML_TOOL_KWD not in table: + if TOOL not in table: return [] - if TOML_POETRY_KWD not in table[TOML_TOOL_KWD]: + if POETRY not in table[TOOL]: return [] - if TOML_DEPENDENCIES_KWD not in table[TOML_TOOL_KWD][TOML_POETRY_KWD]: + if DEPENDENCIES not in table[TOOL][POETRY]: return [] - table[TOML_TOOL_KWD][TOML_POETRY_KWD][TOML_DEPENDENCIES_KWD][TOML_PYTHON_KWD] = new_value + table[TOOL][POETRY][DEPENDENCIES][PYTHON] = new_value return dumps(table).split('\n') @@ -328,17 +359,17 @@ def _set_setuptools_flit_python_requires( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - if TOML_PROJECT_KWD not in table: + if PROJECT not in table: return [] - if TOML_PYTHON_REQUIRES_KWD not in table[TOML_PROJECT_KWD]: + if PYTHON_REQUIRES not in table[PROJECT]: return [] - table[TOML_PROJECT_KWD][TOML_PYTHON_REQUIRES_KWD] = new_value + table[PROJECT][PYTHON_REQUIRES] = new_value return dumps(table).split('\n') -def _update_pyproject_toml_python_requires( - filename: FileOrFilename, - new_value: Union[str, List[str]], +def _update_pyproject_python_requires( + filename: FileOrFilename, + new_value: Union[str, List[str]], ) -> Optional[FileLines]: _updated_table = [] table = load_toml(filename) diff --git a/tests/sources/test_pyproject_flit.py b/tests/sources/test_pyproject_flit.py index fd7593c..f052e13 100644 --- a/tests/sources/test_pyproject_flit.py +++ b/tests/sources/test_pyproject_flit.py @@ -3,8 +3,6 @@ from tomlkit import dumps from typing import List -import pytest - from check_python_versions.sources.pyproject import ( get_python_requires, get_supported_python_versions, @@ -47,7 +45,8 @@ def test_get_supported_python_versions(tmp_path): 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']) + assert get_supported_python_versions(str(filename)) == \ + v(['2.7', '3.6', '3.10']) def test_get_supported_python_versions_keep_comments(tmp_path): @@ -66,18 +65,19 @@ def test_get_supported_python_versions_keep_comments(tmp_path): 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"', - ''] + 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): @@ -128,7 +128,8 @@ def test_get_python_requires_not_specified(tmp_path, capsys): 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' + 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): @@ -243,7 +244,8 @@ def test_update_python_requires_multiline_error(capsys): 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.*, ' + ' 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"]', diff --git a/tests/sources/test_pyproject_poetry.py b/tests/sources/test_pyproject_poetry.py index 06f19e4..57a1fbf 100644 --- a/tests/sources/test_pyproject_poetry.py +++ b/tests/sources/test_pyproject_poetry.py @@ -3,8 +3,6 @@ from tomlkit import dumps from typing import List -import pytest - from check_python_versions.sources.pyproject import ( get_python_requires, get_supported_python_versions, @@ -44,7 +42,8 @@ def test_get_supported_python_versions(tmp_path): 'Programming Language :: Python :: 3.10', ] """)) - assert get_supported_python_versions(str(filename)) == v(['2.7', '3.6', '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): @@ -60,15 +59,16 @@ def test_get_supported_python_versions_keep_comments(tmp_path): ] """)) - 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\',', - ' ]', - ''] + 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): @@ -111,7 +111,8 @@ def test_get_python_requires_not_specified(tmp_path, capsys): 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' + 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): @@ -208,7 +209,8 @@ def test_update_python_requires_multiline_error(capsys): 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.*, ' + ' 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.*"', ''] diff --git a/tests/sources/test_pyproject_setuptools.py b/tests/sources/test_pyproject_setuptools.py index bd92d77..f2f8725 100644 --- a/tests/sources/test_pyproject_setuptools.py +++ b/tests/sources/test_pyproject_setuptools.py @@ -3,8 +3,6 @@ from tomlkit import dumps from typing import List -import pytest - from check_python_versions.sources.pyproject import ( get_python_requires, get_supported_python_versions, @@ -47,7 +45,8 @@ def test_get_supported_python_versions(tmp_path): requires = ["setuptools", "setuptools-scm"] build-backend = "setuptools.build_meta" """)) - assert get_supported_python_versions(str(filename)) == v(['2.7', '3.6', '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): @@ -66,18 +65,19 @@ def test_get_supported_python_versions_keep_comments(tmp_path): 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"', - ''] + 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_update_supported_python_versions_not_a_list(tmp_path, capsys): @@ -95,7 +95,7 @@ def test_update_supported_python_versions_not_a_list(tmp_path, capsys): """)) assert get_supported_python_versions(str(filename)) == [] assert ( - "The value passed to classifiers is not a list" + "The value specified for classifiers is not an array" in capsys.readouterr().err ) @@ -128,7 +128,8 @@ def test_get_python_requires_not_specified(tmp_path, capsys): build-backend = "setuptools.build_meta" """)) assert get_python_requires(str(pyproject_toml)) is None - assert capsys.readouterr().err.strip() == 'The value passed to python dependency is not a string' + 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): @@ -143,7 +144,7 @@ def test_get_python_requires_not_a_string(tmp_path, capsys): """)) assert get_python_requires(str(pyproject_toml)) is None assert ( - 'The value passed to python dependency is not a string' + 'The value specified for python dependency is not a string' in capsys.readouterr().err ) @@ -243,8 +244,9 @@ def test_update_python_requires_multiline_error(capsys): 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.*"', + ' 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"', diff --git a/tests/test_cli.py b/tests/test_cli.py index 8d9ea88..ba1124d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -81,7 +81,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 or pyproject.toml -- 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): From c166aed646961c70ba85aa2a7d684792bb7d2c9e Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 16:15:18 +0100 Subject: [PATCH 30/39] new tests for cli --- tests/test_cli.py | 146 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index ba1124d..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) @@ -152,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("""\ @@ -220,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("""\ From f2c7d2171ea1fa37816e06434236247ce85f3738 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 17:07:03 +0100 Subject: [PATCH 31/39] fix after latest changes --- src/check_python_versions/sources/pyproject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index 08cf8d1..ac1ae56 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -239,7 +239,7 @@ def get_supported_python_versions( return [] if not isinstance(classifiers, list): - warn('The value specified for classifiers is not a list') + warn('The value specified for classifiers is not an array') return [] return get_versions_from_classifiers(classifiers) @@ -271,7 +271,7 @@ def update_supported_python_versions( if classifiers is None: return None if not isinstance(classifiers, list): - warn('The value specified for classifiers is not a list') + 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) From 2a0d7d44e142168dc798d2c3cdc1a19055f1ac79 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Wed, 25 Jan 2023 18:11:54 +0100 Subject: [PATCH 32/39] fix test --- tests/sources/test_pyproject_poetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sources/test_pyproject_poetry.py b/tests/sources/test_pyproject_poetry.py index 57a1fbf..4a9a927 100644 --- a/tests/sources/test_pyproject_poetry.py +++ b/tests/sources/test_pyproject_poetry.py @@ -64,7 +64,7 @@ def test_get_supported_python_versions_keep_comments(tmp_path): ' name=\'foo\'', ' # toml comment', ' classifiers=[', - ' \'Programming Language :: Python :: 2.7\',', + ' \'Programming Language :: Python :: 2.7\',', ' \'Programming Language :: Python :: 3.6\',', ' \'Programming Language :: Python :: 3.10\',', ' ]', From 1214b383a71402ec6d876def56e66725dd4bad1c Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Thu, 26 Jan 2023 10:01:49 +0100 Subject: [PATCH 33/39] fix isort stage --- .../sources/pyproject.py | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index ac1ae56..d12b7f7 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -12,36 +12,19 @@ """ from io import StringIO +from typing import List, Optional, TextIO, Union, cast -from tomlkit import dumps -from tomlkit import load -from tomlkit import TOMLDocument - -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, - compute_python_requires, -) -from .base import Source -from ..utils import ( - FileLines, - FileOrFilename, - is_file_object, - open_file, - warn, -) -from ..versions import ( - SortedVersionList, ) +from ..utils import FileLines, FileOrFilename, is_file_object, open_file, warn +from ..versions import SortedVersionList PYPROJECT_TOML = 'pyproject.toml' From 572ca6e34889466b460ba2fc598e488b7c87e90e Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Thu, 26 Jan 2023 10:24:12 +0100 Subject: [PATCH 34/39] install tomlkit for mypy run --- tox.ini | 1 + 1 file changed, 1 insertion(+) 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} From 7e471e4301c1b4994ba1fc858b75f2cfaebce01b Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Thu, 26 Jan 2023 10:24:35 +0100 Subject: [PATCH 35/39] fix mypy checks --- .../sources/pyproject.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index d12b7f7..b08e946 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -51,9 +51,9 @@ def load_toml( filename: FileOrFilename -) -> TOMLDocument: +) -> Optional[TOMLDocument]: """Utility method that returns a TOMLDocument.""" - table = {} + 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: @@ -141,7 +141,7 @@ def _get_poetry_classifiers( if CLASSIFIERS not in \ table[TOOL][POETRY]: return [] - return table[TOOL][POETRY][CLASSIFIERS] + return cast(List[str], table[TOOL][POETRY][CLASSIFIERS]) def _get_setuptools_flit_classifiers( @@ -152,7 +152,7 @@ def _get_setuptools_flit_classifiers( if CLASSIFIERS not in \ table[PROJECT]: return [] - return table[PROJECT][CLASSIFIERS] + return cast(List[str], table[PROJECT][CLASSIFIERS]) def _get_pyproject_toml_classifiers( @@ -181,7 +181,7 @@ def _get_poetry_python_requires( if PYTHON not in \ table[TOOL][POETRY][DEPENDENCIES]: return [] - return table[TOOL][POETRY][DEPENDENCIES][PYTHON] + return cast(List[str], table[TOOL][POETRY][DEPENDENCIES][PYTHON]) def _get_setuptools_flit_python_requires( @@ -192,7 +192,7 @@ def _get_setuptools_flit_python_requires( if PYTHON_REQUIRES not in \ table[PROJECT]: return [] - return table[PROJECT][PYTHON_REQUIRES] + return cast(List[str], table[PROJECT][PYTHON_REQUIRES]) def _get_pyproject_toml_python_requires( @@ -295,7 +295,8 @@ def _set_poetry_classifiers( if POETRY not in table[TOOL]: return [] table[TOOL][POETRY][CLASSIFIERS] = new_value - return dumps(table).split('\n') + _ret = cast(Optional[List[str]], dumps(table).split('\n')) + return _ret def _set_setuptools_flit_classifiers( @@ -307,14 +308,15 @@ def _set_setuptools_flit_classifiers( if CLASSIFIERS not in table[PROJECT]: return [] table[PROJECT][CLASSIFIERS] = new_value - return dumps(table).split('\n') + _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 = [] + _updated_table: Optional[FileLines] = [] table = load_toml(filename) if is_poetry_toml(table): _updated_table = _set_poetry_classifiers(table, new_value) @@ -335,7 +337,8 @@ def _set_poetry_python_requires( if DEPENDENCIES not in table[TOOL][POETRY]: return [] table[TOOL][POETRY][DEPENDENCIES][PYTHON] = new_value - return dumps(table).split('\n') + _ret = cast(Optional[FileLines], dumps(table).split('\n')) + return _ret def _set_setuptools_flit_python_requires( @@ -347,14 +350,15 @@ def _set_setuptools_flit_python_requires( if PYTHON_REQUIRES not in table[PROJECT]: return [] table[PROJECT][PYTHON_REQUIRES] = new_value - return dumps(table).split('\n') + _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 = [] + _updated_table: Optional[FileLines] = [] table = load_toml(filename) if is_poetry_toml(table): _updated_table = _set_poetry_python_requires(table, new_value) From 4539a49d04818c03b1dac933cbe13f8d220e5f25 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Fri, 27 Jan 2023 12:40:03 +0100 Subject: [PATCH 36/39] removed unused pyproject.toml file --- pyproject.toml | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 6e73386..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,37 +0,0 @@ -[tool.poetry] - name = "check-python-versions" - homepage = "https://github.com/mgedmin/check-python-versions" - description = "Compare supported Python versions in setup.py vs tox.ini et al." - authors = ["Marius Gedminas "] - readme = "README.rst" - license = "GPL" - classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License (GPL)', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - ] - packages = [ - { include = "src" }, - { include = "tests", format = "sdist" }, - ] - - [tool.poetry.dependencies] - python = ">=3.7.0,<4.0" - pyyaml = '*' - - [tool.poetry.scripts] - check-python-versions = 'check_python_versions.cli:main' - - -#[build-system] -# requires = ["poetry-core>=1.0.0"] -# build-backend = "poetry.core.masonry.api" From f1cb32e1dc10fa40ba3a85db44a51a3689341aa4 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Fri, 27 Jan 2023 12:41:12 +0100 Subject: [PATCH 37/39] checks done in previous call on the chain --- src/check_python_versions/sources/pyproject.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index b08e946..48b6bef 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -290,10 +290,6 @@ def _set_poetry_classifiers( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - if TOOL not in table: - return [] - if POETRY not in table[TOOL]: - return [] table[TOOL][POETRY][CLASSIFIERS] = new_value _ret = cast(Optional[List[str]], dumps(table).split('\n')) return _ret @@ -303,10 +299,6 @@ def _set_setuptools_flit_classifiers( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - if PROJECT not in table: - return [] - if CLASSIFIERS not in table[PROJECT]: - return [] table[PROJECT][CLASSIFIERS] = new_value _ret = cast(Optional[List[str]], dumps(table).split('\n')) return _ret @@ -330,12 +322,6 @@ def _set_poetry_python_requires( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - if TOOL not in table: - return [] - if POETRY not in table[TOOL]: - return [] - if DEPENDENCIES not in table[TOOL][POETRY]: - return [] table[TOOL][POETRY][DEPENDENCIES][PYTHON] = new_value _ret = cast(Optional[FileLines], dumps(table).split('\n')) return _ret @@ -345,10 +331,6 @@ def _set_setuptools_flit_python_requires( table: TOMLDocument, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - if PROJECT not in table: - return [] - if PYTHON_REQUIRES not in table[PROJECT]: - return [] table[PROJECT][PYTHON_REQUIRES] = new_value _ret = cast(Optional[FileLines], dumps(table).split('\n')) return _ret From de13d7ae0874fef6ff42e0c830cc89f5a55c6a3f Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Fri, 27 Jan 2023 12:41:39 +0100 Subject: [PATCH 38/39] new tests and changes to reach 100% --- .../sources/pyproject.py | 49 +++-- tests/sources/test_pyproject_flit.py | 53 ++++++ tests/sources/test_pyproject_poetry.py | 174 ++++++++++++++++++ tests/sources/test_pyproject_setuptools.py | 66 +++++++ 4 files changed, 315 insertions(+), 27 deletions(-) diff --git a/src/check_python_versions/sources/pyproject.py b/src/check_python_versions/sources/pyproject.py index 48b6bef..2cc3a22 100644 --- a/src/check_python_versions/sources/pyproject.py +++ b/src/check_python_versions/sources/pyproject.py @@ -10,7 +10,7 @@ check-python-versions supports both. """ - +import io from io import StringIO from typing import List, Optional, TextIO, Union, cast @@ -58,7 +58,7 @@ def load_toml( if isinstance(filename, str) or isinstance(filename, StringIO): with open_file(filename) as fp: table = load(fp) - if isinstance(filename, TextIO): + if isinstance(filename, io.TextIOWrapper): table = load(filename) return table @@ -170,35 +170,35 @@ def _get_pyproject_toml_classifiers( def _get_poetry_python_requires( table: TOMLDocument -) -> List[str]: +) -> Optional[str]: if TOOL not in table: - return [] + return None if POETRY not in table[TOOL]: - return [] + return None if DEPENDENCIES not in \ table[TOOL][POETRY]: - return [] + return None if PYTHON not in \ table[TOOL][POETRY][DEPENDENCIES]: - return [] - return cast(List[str], table[TOOL][POETRY][DEPENDENCIES][PYTHON]) + return None + return cast(str, table[TOOL][POETRY][DEPENDENCIES][PYTHON]) def _get_setuptools_flit_python_requires( table: TOMLDocument -) -> List[str]: +) -> Optional[str]: if PROJECT not in table: - return [] + return None if PYTHON_REQUIRES not in \ table[PROJECT]: - return [] - return cast(List[str], table[PROJECT][PYTHON_REQUIRES]) + return None + return cast(str, table[PROJECT][PYTHON_REQUIRES]) def _get_pyproject_toml_python_requires( filename: FileOrFilename = PYPROJECT_TOML -) -> List[str]: - _python_requires = [] +) -> Optional[str]: + _python_requires = None table = load_toml(filename) if is_poetry_toml(table): _python_requires = _get_poetry_python_requires(table) @@ -214,11 +214,8 @@ def get_supported_python_versions( """Extract supported Python versions from classifiers in pyproject.toml.""" classifiers = _get_pyproject_toml_classifiers(filename) - if classifiers is None: - # Note: do not return None because pyproject.toml is - # not an optional source! - # We want errors to show up if pyproject.toml fails to - # declare Python versions in classifiers. + if not classifiers: + # classifier can be an empty list when nothing found return [] if not isinstance(classifiers, list): @@ -234,9 +231,8 @@ def get_python_requires( """Extract supported Python versions from python_requires in pyproject.toml.""" python_requires = _get_pyproject_toml_python_requires(pyproject_toml) - if python_requires is None: - return None - if not isinstance(python_requires, str): + 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) @@ -251,9 +247,8 @@ def update_supported_python_versions( Does not touch the file but returns a list of lines with new file contents. """ classifiers = _get_pyproject_toml_classifiers(filename) - if classifiers is None: - return None - if not isinstance(classifiers, list): + # 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) @@ -269,7 +264,7 @@ def update_python_requires( 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 or python_requires == []: + if python_requires is None: return None comma = ', ' if ',' in python_requires and ', ' not in python_requires: @@ -308,7 +303,7 @@ def _update_pyproject_toml_classifiers( filename: FileOrFilename, new_value: Union[str, List[str]], ) -> Optional[FileLines]: - _updated_table: Optional[FileLines] = [] + _updated_table: Optional[FileLines] = None table = load_toml(filename) if is_poetry_toml(table): _updated_table = _set_poetry_classifiers(table, new_value) diff --git a/tests/sources/test_pyproject_flit.py b/tests/sources/test_pyproject_flit.py index f052e13..09f044e 100644 --- a/tests/sources/test_pyproject_flit.py +++ b/tests/sources/test_pyproject_flit.py @@ -11,10 +11,12 @@ 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 @@ -100,6 +102,57 @@ def test_update_supported_python_versions_not_a_list(tmp_path, capsys): ) +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("""\ diff --git a/tests/sources/test_pyproject_poetry.py b/tests/sources/test_pyproject_poetry.py index 4a9a927..a85cae0 100644 --- a/tests/sources/test_pyproject_poetry.py +++ b/tests/sources/test_pyproject_poetry.py @@ -11,10 +11,12 @@ 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 @@ -88,6 +90,92 @@ def test_update_supported_python_versions_not_a_list(tmp_path, capsys): ) +def test_update_supported_python_versions_poetry(tmp_path, capsys): + filename = tmp_path / "setup.py" + 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 / "setup.py" + 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 / "setup.py" + 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("""\ @@ -104,6 +192,45 @@ def test_get_python_requires(tmp_path, fix_max_python_3_version): ]) +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("""\ @@ -130,6 +257,42 @@ def test_get_python_requires_not_a_string(tmp_path, capsys): ) +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" @@ -215,6 +378,17 @@ def test_update_python_requires_multiline_error(capsys): ''] +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("""\ diff --git a/tests/sources/test_pyproject_setuptools.py b/tests/sources/test_pyproject_setuptools.py index f2f8725..694acde 100644 --- a/tests/sources/test_pyproject_setuptools.py +++ b/tests/sources/test_pyproject_setuptools.py @@ -11,10 +11,12 @@ 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 @@ -80,6 +82,19 @@ def test_get_supported_python_versions_keep_comments(tmp_path): ''] +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("""\ @@ -100,6 +115,57 @@ def test_update_supported_python_versions_not_a_list(tmp_path, capsys): ) +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("""\ From 04c8988bffa70c56a763b5eb6e78b64a7cbb60e6 Mon Sep 17 00:00:00 2001 From: Gabriele Pongelli Date: Fri, 27 Jan 2023 13:45:00 +0100 Subject: [PATCH 39/39] test file names --- tests/sources/test_pyproject_poetry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/sources/test_pyproject_poetry.py b/tests/sources/test_pyproject_poetry.py index a85cae0..e117e1a 100644 --- a/tests/sources/test_pyproject_poetry.py +++ b/tests/sources/test_pyproject_poetry.py @@ -91,7 +91,7 @@ def test_update_supported_python_versions_not_a_list(tmp_path, capsys): def test_update_supported_python_versions_poetry(tmp_path, capsys): - filename = tmp_path / "setup.py" + filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ [tool.poetry] name='foo' @@ -110,7 +110,7 @@ def test_update_supported_python_versions_poetry(tmp_path, capsys): def test_update_supported_python_versions_not_poetry_table(tmp_path, capsys): - filename = tmp_path / "setup.py" + filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ [project] name='foo' @@ -128,7 +128,7 @@ def test_update_supported_python_versions_not_poetry_table(tmp_path, capsys): def test_update_supported_python_versions_poetry_not_list(tmp_path, capsys): - filename = tmp_path / "setup.py" + filename = tmp_path / "pyproject.toml" filename.write_text(textwrap.dedent("""\ [tool.poetry] name='foo'