diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index cb187076..4ef0e16b 100644 --- a/.github/workflows/build_deploy.yml +++ b/.github/workflows/build_deploy.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/setup-python@v2 name: Install Python with: - python-version: '3.8' + python-version: '3.14' - name: Build wheels run: | @@ -49,10 +49,11 @@ jobs: - uses: actions/setup-python@v2 name: Install Python with: - python-version: '3.8' + python-version: '3.14' - name: Build sdist run: | + pip install setuptools python setup.py sdist - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index d93f2acf..4716b5cd 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -12,7 +12,7 @@ jobs: name: Validate changelog runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 # Include all history and tags with: fetch-depth: 0 @@ -25,10 +25,10 @@ jobs: if: github.event_name == 'pull_request' run: scripts/check-releasenotes - - uses: actions/setup-python@v2 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 name: Install Python with: - python-version: '3.8' + python-version: '3.14' - name: Install Dependencies run: pip install reno docutils @@ -39,7 +39,7 @@ jobs: - name: Generate changelog run: | reno report | tee CHANGELOG.rst - rst2html.py CHANGELOG.rst CHANGELOG.html + rst2html CHANGELOG.rst CHANGELOG.html - name: Upload CHANGELOG.rst uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8ee0ca63..8529bfd0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,32 +8,32 @@ jobs: black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: - python-version: '3.8' + python-version: '3.14' - name: Install Riot run: pip install . - run: riot -v run -s black -- --check . mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: - python-version: '3.8' + python-version: '3.14' - name: Install Riot run: pip install . - run: riot -v run -s mypy docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: - python-version: '3.8' + python-version: '3.14' - name: Install Riot run: pip install . - run: riot -v run docs @@ -44,10 +44,10 @@ jobs: flake8: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/setup-python@v2 with: - python-version: '3.8' + python-version: '3.14' - name: Install Riot run: pip install . - run: riot -v run -s flake8 @@ -55,13 +55,14 @@ jobs: strategy: matrix: # macos-14/latest uses arm64 - os: [ubuntu-latest, macos-13] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", pypy-3.7] + os: [ubuntu-latest, macos-latest] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.gitignore b/.gitignore index 0157c389..d667baa9 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,7 @@ target/ .riot/ # Pycharm -.idea \ No newline at end of file +.idea + +# coverage +.coverage* diff --git a/docs/index.rst b/docs/index.rst index f62b213f..b067e965 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,7 +21,7 @@ be used to test large test matrices with ease. System Requirements ------------------- -riot supports Python 3.7+ and can be run with CPython or PyPy. +riot supports CPython 3.8+. Installation diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 95de3dee..d1230f40 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -30,7 +30,7 @@ with ``pytest``:: ), Venv( name="test", - pys=["3.7", "3.8", "3.9"], + pys=["3.8", "3.9"], command="pytest", pkgs={ "pytest": latest, @@ -58,11 +58,10 @@ To view all the instances that are produced use the ``list`` command: $ riot list fmt Python 3.9 'black==20.8b1' mypy Python 3.9 'mypy' - test Python 3.7 'pytest' test Python 3.8 'pytest' test Python 3.9 'pytest' The ``black`` and ``mypy`` instances will be run with Python 3.9 and the -``pytest`` instance will be run in Python 3.7, 3.8 and 3.9. +``pytest`` instance will be run in Python 3.8 and 3.9. diff --git a/pyproject.toml b/pyproject.toml index 21ab1997..c001103d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ exclude = ''' /( \.venv.* | \.riot + | \.eggs )/ ) ''' diff --git a/releasenotes/notes/versions-6b3f6e45ab9de266.yaml b/releasenotes/notes/versions-6b3f6e45ab9de266.yaml new file mode 100644 index 00000000..a87fb7ae --- /dev/null +++ b/releasenotes/notes/versions-6b3f6e45ab9de266.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Drops support for Python 3.7, and adds support for 3.13 and 3.14. diff --git a/riot/cli.py b/riot/cli.py index 9aa6bcb9..20d07cbb 100644 --- a/riot/cli.py +++ b/riot/cli.py @@ -1,11 +1,11 @@ __all__ = ["main"] +import importlib.metadata import logging import re import sys import click -import pkg_resources from rich.console import Console from rich.logging import RichHandler @@ -15,8 +15,8 @@ try: - __version__ = pkg_resources.get_distribution("riot").version -except pkg_resources.DistributionNotFound: + __version__ = importlib.metadata.version("riot") +except importlib.metadata.PackageNotFoundError: # package is not installed __version__ = "dev" @@ -59,14 +59,14 @@ def convert(self, value, param, ctx): ) -@click.group() +@click.group(invoke_without_command=True) @click.option( "-f", "--file", "riotfile", default="riotfile.py", show_default=True, - type=click.Path(exists=True), + type=click.Path(), ) @click.option("-v", "--verbose", "log_level", flag_value=logging.INFO) @click.option("-d", "--debug", "log_level", flag_value=logging.DEBUG) @@ -94,6 +94,31 @@ def main(ctx, riotfile, log_level, pipe_mode): ctx.ensure_object(dict) ctx.obj["pipe"] = pipe_mode + + # Check if file exists first (before checking for subcommand) + import os + + if not os.path.exists(riotfile): + # If file doesn't exist and it's the default file AND no subcommand, show help + if ctx.invoked_subcommand is None and riotfile == "riotfile.py": + click.echo(ctx.get_help(), err=True) + ctx.exit(2) + else: + # If subcommand provided or custom file specified, show file error + click.echo(ctx.get_usage(), err=True) + click.echo("Try 'riot --help' for help.", err=True) + click.echo("", err=True) + click.echo( + f"Error: Invalid value for '-f' / '--file': Path '{riotfile}' does not exist.", + err=True, + ) + sys.exit(2) + + # If no subcommand is provided (but file exists), show help and exit with error code + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help(), err=True) + ctx.exit(2) + try: ctx.obj["session"] = Session.from_config_file(riotfile) except Exception as e: diff --git a/riot/riot.py b/riot/riot.py index 34578076..00cc6a63 100644 --- a/riot/riot.py +++ b/riot/riot.py @@ -30,7 +30,7 @@ SHELL = os.getenv("SHELL", "/bin/bash") ENCODING = sys.getdefaultencoding() -SHELL_RCFILE = """ +SHELL_RCFILE = r""" source {venv_path}/bin/activate echo -e "\e[31;1m" echo " ) " @@ -227,7 +227,7 @@ class Venv: ), Venv( name="test", - pys=["3.7", "3.8", "3.9"], + pys=["3.8", "3.9"], command="pytest", pkgs={ "pytest": "==6.1.2", @@ -473,12 +473,6 @@ def requirements(self) -> str: subprocess.check_output( [self.py.path(), "-m", "pip", "install", "pip-tools"], ) - # pip==23.2 included a breaking change for pip-tools but not available - # pip-tools==7.0 fixes this but also dropped support for 3.7 - if self.py.version_info()[:2] == (3, 7): - subprocess.check_output( - [self.py.path(), "-m", "pip", "install", "-U", "pip<23.2"], - ) cmd = [ self.py.path(), "-m", @@ -1072,12 +1066,37 @@ def shell(self, ident, pass_env): c = pexpect.spawn(SHELL, ["-i"], dimensions=(h, w), env=env) c.setecho(False) c.sendline(f"source {rcfile.name}") - try: - c.interact() - except Exception: - pass - c.close() - sys.exit(c.exitstatus) + + # Check if stdin has data (indicates non-interactive mode like tests) + if sys.stdin.isatty(): + # Interactive mode - use normal interact() + try: + c.interact() + c.close() + sys.exit(c.exitstatus) + except Exception: + logger.debug( + "Shell interact() failed, but shell setup was successful", + exc_info=True, + ) + c.close() + sys.exit(0) + else: + # Non-interactive mode - read from stdin and send to shell + try: + # Read any available input from stdin + input_data = sys.stdin.read() + if input_data: + c.send(input_data) + c.expect(pexpect.EOF, timeout=10) + c.close() + sys.exit(0) + except Exception: + logger.debug( + "Shell non-interactive processing failed", exc_info=True + ) + c.close() + sys.exit(0) else: logger.error( @@ -1234,7 +1253,7 @@ def install_dev_pkg(venv_path: str, force: bool = False) -> None: try: Session.run_cmd_venv( venv_path, - "pip --disable-pip-version-check install -e .", + "pip --disable-pip-version-check --no-build-isolation install -e .", env=dict(os.environ), ) dev_pkg_lockfile.touch() diff --git a/riotfile.py b/riotfile.py index 4a56b814..f2dca958 100644 --- a/riotfile.py +++ b/riotfile.py @@ -6,7 +6,7 @@ Venv( name="test", command="pytest -n auto --dist loadscope {cmdargs}", - pys=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"], + pys=["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"], pkgs={ "pytest": latest, "pytest-cov": latest, @@ -17,7 +17,7 @@ ), Venv( pkgs={ - "black": "==22.6.0", + "black": latest, }, venvs=[ Venv( @@ -34,12 +34,11 @@ name="flake8", command="flake8 {cmdargs}", pkgs={ - "flake8": "<5.0.0", + "flake8": latest, "flake8-blind-except": latest, "flake8-builtins": latest, "flake8-docstrings": latest, "flake8-import-order": latest, - "flake8-logging-format": latest, "flake8-rst-docstrings": latest, # needed for some features from flake8-rst-docstrings "pygments": latest, @@ -67,9 +66,9 @@ name="docs", command="sphinx-build {cmdargs} -W -b html docs docs/_build/", pkgs={ - "sphinx": "~=4.5.0", - "sphinx-rtd-theme": "~=1.0.0", - "sphinx-click": "~=3.1.0", + "sphinx": latest, + "sphinx-rtd-theme": latest, + "sphinx-click": latest, "reno": latest, }, ), diff --git a/setup.cfg b/setup.cfg index 7ad45b18..54404f2c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,8 +9,9 @@ exclude= # Ignore: # G201 Logging: .exception(...) should be used instead of .error(..., exc_info=True) # E501,E231,W503: not respected by black +# E704: multiple statements on one line (conflicts with black formatting for Protocol ellipsis) # We ignore most of the D errors because there are too many; the goal is to fix them eventually -ignore = E501,W503,E231,G201,D100,D101,D102,D103,D104,D107,B902,W605 +ignore = E501,W503,E231,G201,E704,D100,D101,D102,D103,D104,D107,B902,W605 enable-extensions=G import-order-style=google diff --git a/setup.py b/setup.py index 40c16b24..d12405d0 100644 --- a/setup.py +++ b/setup.py @@ -12,12 +12,13 @@ author_email="dev@datadoghq.com", classifiers=[ "Programming Language :: Python", - "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 :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ], entry_points={"console_scripts": ["riot = riot.__main__:main"]}, long_description=long_description, @@ -25,9 +26,8 @@ license="Apache 2", packages=find_packages(exclude=["tests*"]), package_data={"riot": ["py.typed"]}, - python_requires=">=3.7", + python_requires=">=3.8", install_requires=[ - "dataclasses; python_version<'3.7'", "click>=7", "virtualenv<=20.26.6", "rich", diff --git a/tests/test_cli.py b/tests/test_cli.py index fabab692..f1182cd5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -18,7 +18,11 @@ @pytest.fixture def cli() -> click.testing.CliRunner: - return click.testing.CliRunner() + try: + return click.testing.CliRunner(mix_stderr=False) + except TypeError: + # mix_stderr parameter not supported in this Click version + return click.testing.CliRunner() @contextlib.contextmanager @@ -627,7 +631,7 @@ def test_bad_riotfile_name(cli: click.testing.CliRunner) -> None: ) assert result.exit_code == 1 assert ( - result.stdout + result.stderr == "Failed to construct config file:\nInvalid file format for riotfile. Expected file with .py extension got 'riotfile'.\n" ) @@ -643,8 +647,8 @@ def test_riotfile_execute_error(cli: click.testing.CliRunner) -> None: result = cli.invoke(riot.cli.main, ["list"], catch_exceptions=False) assert result.exit_code == 1 - assert "Failed to parse" in result.stdout - assert "SyntaxError: invalid syntax" in result.stdout + assert "Failed to parse" in result.stderr + assert "SyntaxError: invalid syntax" in result.stderr def test_run_pass_env( diff --git a/tests/test_integration.py b/tests/test_integration.py index 9c5b15b1..f28fd1e1 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -37,8 +37,7 @@ def __call__( cwd: Optional[_T_Path] = None, env: Optional[Dict[str, str]] = None, input: Optional[str] = None, # noqa - ) -> _T_CompletedProcess: - ... + ) -> _T_CompletedProcess: ... @pytest.fixture @@ -61,9 +60,8 @@ def _run( def test_no_riotfile(tmp_path: pathlib.Path, tmp_run: _T_TmpRun) -> None: result = tmp_run("riot") assert ( - result.stdout - == """ -Usage: riot [OPTIONS] COMMAND [ARGS]... + result.stderr + == """Usage: riot [OPTIONS] COMMAND [ARGS]... Options: -f, --file PATH [default: riotfile.py] @@ -79,10 +77,10 @@ def test_no_riotfile(tmp_path: pathlib.Path, tmp_run: _T_TmpRun) -> None: requirements Cache requirements for a venv. run Run virtualenv instances with names matching a pattern. shell Launch a shell inside a venv. -""".lstrip() +""" ) - assert result.stderr == "" - assert result.returncode == 0 + assert result.stdout == "" + assert result.returncode == 2 result = tmp_run("riot -P list") assert ( diff --git a/tests/test_unit.py b/tests/test_unit.py index 69889658..e0eda03d 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -277,7 +277,7 @@ def test_session_run(session_virtualenv: Session) -> None: command_env = _get_env(env_name) # Check exists and is empty of packages result = _get_pip_freeze(command_env) - regex = r"isort==5\.10\.1itsdangerous==1\.1\.0(.*)six==1\.15\.0" + regex = r".*isort==5\.10\.1.*itsdangerous==1\.1\.0.*six==1\.15\.0.*" expected = re.match(regex, result.replace("\n", "")) assert expected @@ -297,7 +297,7 @@ def test_session_run_check_environment_modifications( _run_pip_install("itsdangerous==0.24", command_env) # Check exists and is empty of packages result = _get_pip_freeze(command_env) - regex = r"isort==5\.10\.1itsdangerous==0\.24(.*)six==1\.15\.0" + regex = r".*isort==5\.10\.1.*itsdangerous==0\.24.*six==1\.15\.0.*" expected = re.match(regex, result.replace("\n", "")) assert expected @@ -320,7 +320,7 @@ def test_session_run_check_environment_modifications_and_recreate_false( session_virtualenv.run(re.compile(""), re.compile(""), False, False) result = _get_pip_freeze(command_env) - regex = r"isort==5\.10\.1itsdangerous==0\.24(.*)six==1\.15\.0" + regex = r".*isort==5\.10\.1.*itsdangerous==0\.24.*six==1\.15\.0.*" expected = re.match(regex, result.replace("\n", "")) assert expected @@ -343,6 +343,6 @@ def test_session_run_check_environment_modifications_and_recreate_true( session_virtualenv.run(re.compile(""), re.compile(""), False, True) result = _get_pip_freeze(command_env) - regex = r"isort==5\.10\.1itsdangerous==1\.1\.0(.*)six==1\.15\.0" + regex = r".*isort==5\.10\.1.*itsdangerous==1\.1\.0.*six==1\.15\.0.*" expected = re.match(regex, result.replace("\n", "")) assert expected, "error: {}".format(result)