diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ac4920d0..2ed5d4a0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 dependencies installed by connect to run the deployed content - `rsconnect content venv` command recreates a local python environment equal to the one used by connect to run the content. +- Added `--requirements-file` option on deploy and write-manifest commands to + supply an explicit requirements file instead of detecting the environment. ## [1.28.2] - 2025-12-05 diff --git a/rsconnect/actions.py b/rsconnect/actions.py index bb8b65a5..a16e7359 100644 --- a/rsconnect/actions.py +++ b/rsconnect/actions.py @@ -382,8 +382,8 @@ def deploy_app( environment = Environment.create_python_environment( directory, # pyright: ignore - force_generate, - python, + requirements_file="requirements.txt" if not force_generate else None, + python=python, ) # At this point, kwargs has a lot of things, but we can need to prune it down to just the things that diff --git a/rsconnect/environment.py b/rsconnect/environment.py index 270a0620..d2d2ca31 100644 --- a/rsconnect/environment.py +++ b/rsconnect/environment.py @@ -114,7 +114,7 @@ def from_dict( def create_python_environment( cls, directory: str, - force_generate: bool = False, + requirements_file: typing.Optional[str] = "requirements.txt", python: typing.Optional[str] = None, override_python_version: typing.Optional[str] = None, app_file: typing.Optional[str] = None, @@ -125,8 +125,8 @@ def create_python_environment( If no Python executable is provided, the current system Python executable is used. :param directory: the project directory to inspect. - :param force_generate: force generating "requirements.txt" to snapshot the environment - packages even if it already exists. + :param requirements_file: requirements file name relative to the project directory. If None, + capture the environment via pip freeze. :param python: the Python executable of the environment to use for inspection. :param override_python_version: the Python version required by the project. :param app_file: the main application file to use for inspection. @@ -138,9 +138,8 @@ def create_python_environment( else: module_file = app_file - # click.secho(' Deploying %s to server "%s"' % (directory, connect_server.url)) _warn_on_ignored_manifest(directory) - _warn_if_no_requirements_file(directory) + _warn_if_no_requirements_file(directory, requirements_file) _warn_if_environment_directory(directory) python_version_requirement = pyproject.detect_python_version_requirement(directory) @@ -163,7 +162,7 @@ def create_python_environment( python_version_requirement = f"=={override_python_version}" # with cli_feedback("Inspecting Python environment"): - environment = cls._get_python_env_info(module_file, python, force_generate) + environment = cls._get_python_env_info(module_file, python, requirements_file=requirements_file) environment.python_version_requirement = python_version_requirement if override_python_version: @@ -181,14 +180,17 @@ def create_python_environment( # Derive allow_uv from selection environment.package_manager_allow_uv = selected_package_manager is PackageInstaller.UV - if force_generate: + if requirements_file is None: _warn_on_ignored_requirements(directory, environment.filename) return environment @classmethod def _get_python_env_info( - cls, file_name: str, python: typing.Optional[str], force_generate: bool = False + cls, + file_name: str, + python: typing.Optional[str], + requirements_file: typing.Optional[str] = "requirements.txt", ) -> "Environment": """ Gathers the python and environment information relating to the specified file @@ -196,14 +198,13 @@ def _get_python_env_info( :param file_name: the primary file being deployed. :param python: the optional name of a Python executable. - :param force_generate: force generating "requirements.txt" or "environment.yml", - even if it already exists. + :param requirements_file: which requirements file to read. If None, generate via pip freeze. :return: information about the version of Python in use plus some environmental stuff. """ python = which_python(python) logger.debug("Python: %s" % python) - environment = cls._inspect_environment(python, os.path.dirname(file_name), force_generate=force_generate) + environment = cls._inspect_environment(python, os.path.dirname(file_name), requirements_file=requirements_file) if environment.error: raise RSConnectException(environment.error) logger.debug("Python: %s" % python) @@ -215,7 +216,7 @@ def _inspect_environment( cls, python: str, directory: str, - force_generate: bool = False, + requirements_file: typing.Optional[str] = "requirements.txt", check_output: typing.Callable[..., bytes] = subprocess.check_output, ) -> "Environment": """Run the environment inspector using the specified python binary. @@ -223,13 +224,8 @@ def _inspect_environment( Returns a dictionary of information about the environment, or containing an "error" field if an error occurred. """ - flags: typing.List[str] = [] - if force_generate: - flags.append("f") - args = [python, "-m", "rsconnect.subprocesses.inspect_environment"] - if flags: - args.append("-" + "".join(flags)) + args.extend(["--requirements-file", requirements_file or "none"]) args.append(directory) try: @@ -321,17 +317,28 @@ def _warn_on_ignored_manifest(directory: str) -> None: ) -def _warn_if_no_requirements_file(directory: str) -> None: +def _warn_if_no_requirements_file(directory: str, requirements_file: typing.Optional[str]) -> None: """ - Checks for the existence of a file called requirements.txt in the given directory. - If it's not there, a warning will be printed. + Check that a requirements file exists, and that it lives inside the deployment directory. :param directory: the directory to check in. + :param requirements_file: the name of the requirements file, or None to skip the check. """ - if not os.path.exists(os.path.join(directory, "requirements.txt")): + if requirements_file is None: + return + + directory_path = pathlib.Path(directory) + requirements_file_path = directory_path / pathlib.Path(requirements_file) + if directory_path not in requirements_file_path.parents: + click.secho( + " Warning: The requirements file '%s' is outside of the deployment directory.\n" % requirements_file, + fg="red", + ) + + if not requirements_file_path.exists(): click.secho( " Warning: Capturing the environment using 'pip freeze'.\n" - " Consider creating a requirements.txt file instead.", + " Consider creating a %s file instead." % requirements_file, fg="yellow", ) diff --git a/rsconnect/main.py b/rsconnect/main.py index 5ba24c59..2bab7e17 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -1018,6 +1018,16 @@ def _warn_on_ignored_requirements(directory: str, requirements_file_name: str): is_flag=True, help='Force generating "requirements.txt", even if it already exists.', ) +@click.option( + "--requirements-file", + "-r", + type=click.Path(exists=True, dir_okay=False), + default="requirements.txt", + help=( + "Path to requirements file to record in the manifest instead of detecting the environment. " + "Must be inside the notebook directory. Use 'none' to capture via pip freeze." + ), +) @click.option( "--package-installer", type=click.Choice(PackageInstaller), @@ -1050,6 +1060,7 @@ def deploy_notebook( python: Optional[str], override_python_version: Optional[str], force_generate: bool, + requirements_file: Optional[str], verbose: int, file: str, extra_files: tuple[str, ...], @@ -1073,18 +1084,16 @@ def deploy_notebook( app_mode = AppModes.JUPYTER_NOTEBOOK if not static else AppModes.STATIC base_dir = dirname(file) + requirements_file = resolve_requirements_file(base_dir, requirements_file, force_generate) environment = Environment.create_python_environment( base_dir, + requirements_file=requirements_file, app_file=file, - force_generate=force_generate, python=python, override_python_version=override_python_version, package_manager=package_installer, ) - if force_generate: - _warn_on_ignored_requirements(base_dir, environment.filename) - ce = RSConnectExecutor( ctx=ctx, name=name, @@ -1179,6 +1188,16 @@ def deploy_notebook( is_flag=True, help='Force generating "requirements.txt", even if it already exists.', ) +@click.option( + "--requirements-file", + "-r", + type=click.Path(exists=True, dir_okay=False), + default="requirements.txt", + help=( + "Path to requirements file to record in the manifest instead of detecting the environment. " + "Must be inside the notebook directory. Use 'none' to capture via pip freeze." + ), +) @click.option( "--package-installer", type=click.Choice(PackageInstaller), @@ -1199,6 +1218,7 @@ def deploy_voila( python: Optional[str], override_python_version: Optional[str], force_generate: bool, + requirements_file: Optional[str], extra_files: tuple[str, ...], exclude: tuple[str, ...], image: Optional[str], @@ -1225,11 +1245,13 @@ def deploy_voila( set_verbosity(verbose) output_params(ctx, locals().items()) app_mode = AppModes.JUPYTER_VOILA + base_dir = path if isdir(path) else dirname(path) + requirements_file = resolve_requirements_file(base_dir, requirements_file, force_generate) environment = Environment.create_python_environment( - path if isdir(path) else dirname(path), - force_generate, - python, - override_python_version, + base_dir, + requirements_file=requirements_file, + python=python, + override_python_version=override_python_version, package_manager=package_installer, ) @@ -1256,7 +1278,7 @@ def deploy_voila( entrypoint, extra_files, exclude, - force_generate, + requirements_file is None, environment, image=image, env_management_py=env_management_py, @@ -1400,6 +1422,16 @@ def deploy_manifest( is_flag=True, help='Force generating "requirements.txt", even if it already exists.', ) +@click.option( + "--requirements-file", + "-r", + type=click.Path(exists=True, dir_okay=False), + default="requirements.txt", + help=( + "Path to requirements file to record in the manifest instead of detecting the environment. " + "Must be inside the project directory. Use 'none' to capture via pip freeze." + ), +) @click.option( "--package-installer", type=click.Choice(PackageInstaller), @@ -1429,6 +1461,7 @@ def deploy_quarto( python: Optional[str], override_python_version: Optional[str], force_generate: bool, + requirements_file: Optional[str], verbose: int, file_or_directory: str, extra_files: Sequence[str], @@ -1459,9 +1492,12 @@ def deploy_quarto( environment = None if "jupyter" in engines: + requirements_file = resolve_requirements_file(base_dir, requirements_file, force_generate) with cli_feedback("Inspecting Python environment"): environment = Environment.create_python_environment( - base_dir, force_generate=force_generate, override_python_version=override_python_version + base_dir, + requirements_file=requirements_file, + override_python_version=override_python_version, ) ce = RSConnectExecutor( @@ -1713,6 +1749,19 @@ def deploy_html( ce.verify_deployment() +def resolve_requirements_file(directory: str, requirements_file: Optional[str], force_generate: bool) -> Optional[str]: + """ + Determine which requirements file to use. + + Returns None when pip freeze should be used (force_generate=True), otherwise returns + the provided path or the default "requirements.txt". + """ + if force_generate: + _warn_on_ignored_requirements(directory, requirements_file or "requirements.txt") + return None + return requirements_file or "requirements.txt" + + def generate_deploy_python(app_mode: AppMode, alias: str, min_version: str, desc: Optional[str] = None): if desc is None: desc = app_mode.desc() @@ -1772,6 +1821,15 @@ def generate_deploy_python(app_mode: AppMode, alias: str, min_version: str, desc is_flag=True, help='Force generating "requirements.txt", even if it already exists.', ) + @click.option( + "--requirements-file", + "-r", + type=click.Path(exists=True, dir_okay=False), + help=( + "Path to requirements file to record in the manifest instead of detecting the environment. " + "Must be inside the deployment directory. Use 'none' to capture via pip freeze." + ), + ) @click.option( "--package-installer", type=click.Choice(PackageInstaller), @@ -1804,6 +1862,7 @@ def deploy_app( python: Optional[str], override_python_version: Optional[str], force_generate: bool, + requirements_file: Optional[str], verbose: int, directory: str, extra_files: tuple[str, ...], @@ -1823,10 +1882,11 @@ def deploy_app( set_verbosity(verbose) entrypoint = validate_entry_point(entrypoint, directory) extra_files_list = validate_extra_files(directory, extra_files) + requirements_file = resolve_requirements_file(directory, requirements_file, force_generate) environment = Environment.create_python_environment( directory, - force_generate, - python, + requirements_file=requirements_file, + python=python, override_python_version=override_python_version, package_manager=package_installer, ) @@ -1969,6 +2029,15 @@ def write_manifest(): is_flag=True, help='Force generating "requirements.txt", even if it already exists.', ) +@click.option( + "--requirements-file", + "-r", + type=click.Path(exists=True, dir_okay=False), + help=( + "Path to requirements file to record in the manifest instead of detecting the environment. " + "Must be inside the notebook directory. Use 'none' to capture via pip freeze." + ), +) @click.option( "--package-installer", type=click.Choice(PackageInstaller), @@ -2001,6 +2070,7 @@ def write_manifest_notebook( hide_all_input: Optional[bool] = None, hide_tagged_input: Optional[bool] = None, package_installer: Optional[PackageInstaller] = None, + requirements_file: Optional[str] = None, ): set_verbosity(verbose) output_params(ctx, locals().items()) @@ -2013,16 +2083,18 @@ def write_manifest_notebook( if exists(manifest_path) and not overwrite: raise RSConnectException("manifest.json already exists. Use --overwrite to overwrite.") + requirements_file = resolve_requirements_file(base_dir, requirements_file, force_generate) with cli_feedback("Inspecting Python environment"): environment = Environment.create_python_environment( base_dir, - force_generate=force_generate, + requirements_file=requirements_file, python=python, override_python_version=override_python_version, app_file=file, package_manager=package_installer, ) + generate_env = requirements_file is None with cli_feedback("Creating manifest.json"): environment_file_exists = write_notebook_manifest_json( file, @@ -2036,7 +2108,7 @@ def write_manifest_notebook( env_management_r, ) - if environment_file_exists and not force_generate: + if environment_file_exists and not generate_env: click.secho( " Warning: %s already exists and will not be overwritten." % environment.filename, fg="yellow", @@ -2074,6 +2146,15 @@ def write_manifest_notebook( is_flag=True, help='Force generating "requirements.txt", even if it already exists.', ) +@click.option( + "--requirements-file", + "-r", + type=click.Path(exists=True, dir_okay=False), + help=( + "Path to requirements file to record in the manifest instead of detecting the environment. " + "Must be inside the notebook directory. Use 'none' to capture via pip freeze." + ), +) @click.option( "--package-installer", type=click.Choice(PackageInstaller), @@ -2122,6 +2203,7 @@ def write_manifest_voila( env_management_r: Optional[bool], multi_notebook: bool, package_installer: Optional[PackageInstaller] = None, + requirements_file: Optional[str] = None, ): set_verbosity(verbose) output_params(ctx, locals().items()) @@ -2132,10 +2214,11 @@ def write_manifest_voila( if exists(manifest_path) and not overwrite: raise RSConnectException("manifest.json already exists. Use --overwrite to overwrite.") + requirements_file = resolve_requirements_file(base_dir, requirements_file, force_generate) with cli_feedback("Inspecting Python environment"): environment = Environment.create_python_environment( base_dir, - force_generate=force_generate, + requirements_file=requirements_file, override_python_version=override_python_version, python=python, app_file=path, @@ -2143,8 +2226,8 @@ def write_manifest_voila( ) environment_file_exists = exists(join(base_dir, environment.filename)) - - if environment_file_exists and not force_generate: + generate_env = requirements_file is None + if environment_file_exists and not generate_env: click.secho( " Warning: %s already exists and will not be overwritten." % environment.filename, fg="yellow", @@ -2160,7 +2243,7 @@ def write_manifest_voila( environment, extra_files, exclude, - force_generate, + generate_env, image, env_management_py, env_management_r, @@ -2216,6 +2299,15 @@ def write_manifest_voila( is_flag=True, help='Force generating "requirements.txt", even if it already exists.', ) +@click.option( + "--requirements-file", + "-r", + type=click.Path(exists=True, dir_okay=False), + help=( + "Path to requirements file to record in the manifest instead of detecting the environment. " + "Must be inside the project directory." + ), +) @click.option( "--package-installer", type=click.Choice(PackageInstaller), @@ -2246,6 +2338,7 @@ def write_manifest_quarto( env_management_py: Optional[bool], env_management_r: Optional[bool], package_installer: Optional[PackageInstaller], + requirements_file: Optional[str], ): set_verbosity(verbose) output_params(ctx, locals().items()) @@ -2265,20 +2358,27 @@ def write_manifest_quarto( logger.debug("Quarto: %s" % quarto) inspect = quarto_inspect(quarto, file_or_directory) engines = validate_quarto_engines(inspect) + if requirements_file and "jupyter" not in engines: + raise RSConnectException( + "--requirements-file is only supported for Quarto content using the Jupyter engine." + ) environment = None + generate_env = False if "jupyter" in engines: + requirements_file = resolve_requirements_file(base_dir, requirements_file, force_generate) + generate_env = requirements_file is None with cli_feedback("Inspecting Python environment"): environment = Environment.create_python_environment( base_dir, - force_generate=force_generate, + requirements_file=requirements_file, override_python_version=override_python_version, python=python, package_manager=package_installer, ) environment_file_exists = exists(join(base_dir, environment.filename)) - if environment_file_exists and not force_generate: + if environment_file_exists and not generate_env: click.secho( " Warning: %s already exists and will not be overwritten." % environment.filename, fg="yellow", @@ -2414,6 +2514,15 @@ def generate_write_manifest_python(app_mode: AppMode, alias: str, desc: Optional is_flag=True, help='Force generating "requirements.txt", even if it already exists.', ) + @click.option( + "--requirements-file", + "-r", + type=click.Path(exists=True, dir_okay=False), + help=( + "Path to requirements file to record in the manifest instead of detecting the environment. " + "Must be inside the application directory. Use 'none' to capture via pip freeze." + ), + ) @click.option( "--package-installer", type=click.Choice(PackageInstaller), @@ -2446,7 +2555,9 @@ def manifest_writer( env_management_py: Optional[bool], env_management_r: Optional[bool], package_installer: Optional[PackageInstaller], + requirements_file: Optional[str], ): + resolved_requirements_file = resolve_requirements_file(directory, requirements_file, force_generate) _write_framework_manifest( ctx, overwrite, @@ -2454,7 +2565,6 @@ def manifest_writer( exclude, python, override_python_version, - force_generate, verbose, directory, extra_files, @@ -2463,6 +2573,7 @@ def manifest_writer( env_management_py, env_management_r, package_installer=package_installer, + requirements_file=resolved_requirements_file, ) return manifest_writer @@ -2487,7 +2598,6 @@ def _write_framework_manifest( exclude: tuple[str, ...], python: Optional[str], override_python_version: Optional[str], - force_generate: bool, verbose: int, directory: str, extra_files: tuple[str, ...], @@ -2496,6 +2606,7 @@ def _write_framework_manifest( env_management_py: Optional[bool], env_management_r: Optional[bool], package_installer: Optional[PackageInstaller] = None, + requirements_file: Optional[str] = None, ): """ A common function for writing manifests for APIs as well as Dash, Streamlit, Bokeh, and Panel apps. @@ -2506,8 +2617,6 @@ def _write_framework_manifest( the deploy. :param python: a path to the Python executable to use. :param override_python_version: Python version number Connect should use instead of the locally-installed version - :param force_generate: a flag to force the generation of manifest and - requirements file. :param verbose: a flag to produce more (debugging) output. :param directory: the directory of the thing to deploy. :param extra_files: any extra files that should be included. @@ -2528,10 +2637,11 @@ def _write_framework_manifest( if exists(manifest_path) and not overwrite: raise RSConnectException("manifest.json already exists. Use --overwrite to overwrite.") + resolved_requirements_file = resolve_requirements_file(directory, requirements_file, False) with cli_feedback("Inspecting Python environment"): environment = Environment.create_python_environment( directory, - force_generate=force_generate, + requirements_file=resolved_requirements_file, override_python_version=override_python_version, python=python, ) @@ -2554,7 +2664,8 @@ def _write_framework_manifest( env_management_r, ) - if environment_file_exists and not force_generate: + generate_env = resolved_requirements_file is None + if environment_file_exists and not generate_env: click.secho( " Warning: %s already exists and will not be overwritten." % environment.filename, fg="yellow", diff --git a/rsconnect/subprocesses/inspect_environment.py b/rsconnect/subprocesses/inspect_environment.py index 97b2c9a4..92af5452 100644 --- a/rsconnect/subprocesses/inspect_environment.py +++ b/rsconnect/subprocesses/inspect_environment.py @@ -8,6 +8,7 @@ """ from __future__ import annotations +import argparse import datetime import json import locale @@ -60,21 +61,21 @@ class EnvironmentException(Exception): pass -def detect_environment(dirname: str, force_generate: bool = False) -> EnvironmentData: +def detect_environment(dirname: str, requirements_file: Optional[str] = "requirements.txt") -> EnvironmentData: """Determine the python dependencies in the environment. `pip freeze` will be used to introspect the environment. :param: dirname Directory name - :param: force_generate Force the generation of an environment + :param: requirements_file The requirements file to read. If None, generate using pip freeze. :return: a dictionary containing the package spec filename and contents if successful, or a dictionary containing `error` on failure. """ - if force_generate: + if requirements_file is None: result = pip_freeze() else: - result = output_file(dirname, "requirements.txt", "pip") or pip_freeze() + result = output_file(dirname, requirements_file, "pip") or pip_freeze() if result is not None: result["python"] = get_python_version() @@ -206,17 +207,24 @@ def main(): Run `detect_environment` and dump the result as JSON. """ try: - if len(sys.argv) < 2: - raise EnvironmentException("Usage: %s [-fc] DIRECTORY" % sys.argv[0]) - # directory is always the last argument - directory = sys.argv[len(sys.argv) - 1] - flags = "" - force_generate = False - if len(sys.argv) > 2: - flags = sys.argv[1] - if "f" in flags: - force_generate = True - envinfo = detect_environment(directory, force_generate)._asdict() + parser = argparse.ArgumentParser( + description="Inspect python environment and return dependency metadata.", add_help=True + ) + parser.add_argument( + "-r", + "--requirements-file", + dest="requirements_file", + default="requirements.txt", + help="Requirements file name (relative to the directory). Use 'none' to capture via pip freeze.", + ) + parser.add_argument("directory", help="Directory to inspect.") + args = parser.parse_args() + + requirements_file = args.requirements_file + if requirements_file.lower() == "none": + requirements_file = None + + envinfo = detect_environment(args.directory, requirements_file=requirements_file)._asdict() if "contents" in envinfo: keepers = list(map(strip_ref, envinfo["contents"].split("\n"))) keepers = [line for line in keepers if not exclude(line)] diff --git a/tests/test_environment.py b/tests/test_environment.py index 9d883315..0495fc90 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -2,6 +2,7 @@ import sys import os import tempfile +import shutil import subprocess from unittest import TestCase from unittest import mock @@ -59,6 +60,23 @@ def test_file(self): ) self.assertEqual(expected, result) + def test_requirements_override(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_dir = os.path.join(tmpdir, "project") + shutil.copytree(get_dir("pip1"), project_dir) + os.makedirs(os.path.join(project_dir, "alt"), exist_ok=True) + custom_requirements = os.path.join(project_dir, "alt", "custom.txt") + with open(custom_requirements, "w") as f: + f.write("foo==1.0\nbar>=2.0\nrsconnect==0.1\n") + + result = Environment.create_python_environment( + project_dir, requirements_file=os.path.join("alt", "custom.txt") + ) + + assert result.filename.endswith("custom.txt") + assert result.contents == "foo==1.0\nbar>=2.0\n" + assert result.source == "file" + def test_pip_freeze(self): result = Environment.create_python_environment(get_dir("pip2")) @@ -175,7 +193,7 @@ def test_inspect_environment_catches_type_error(): ( "file_name", "python", - "force_generate", + "requirements_file", "expected_python", "expected_environment", ), @@ -183,7 +201,7 @@ def test_inspect_environment_catches_type_error(): pytest.param( "path/to/file.py", sys.executable, - False, + "requirements.txt", sys.executable, Environment.from_dict( dict( @@ -203,7 +221,7 @@ def test_inspect_environment_catches_type_error(): pytest.param( "another/file.py", os.path.basename(sys.executable), - False, + "requirements.txt", sys.executable, Environment.from_dict( dict( @@ -223,7 +241,7 @@ def test_inspect_environment_catches_type_error(): pytest.param( "will/the/files/never/stop.py", "argh.py", - False, + "requirements.txt", "unused", Environment.from_dict( dict( @@ -245,7 +263,7 @@ def test_get_python_env_info( monkeypatch, file_name, python, - force_generate, + requirements_file, expected_python, expected_environment, ): @@ -255,7 +273,7 @@ def fake_which_python(python, env=os.environ): def fake_inspect_environment( python, directory, - force_generate=False, + requirements_file="requirements.txt", check_output=subprocess.check_output, ): return expected_environment @@ -266,9 +284,9 @@ def fake_inspect_environment( if expected_environment.error is not None: with pytest.raises(RSConnectException): - _ = Environment._get_python_env_info(file_name, python, force_generate=force_generate) + _ = Environment._get_python_env_info(file_name, python, requirements_file=requirements_file) else: - environment = Environment._get_python_env_info(file_name, python, force_generate=force_generate) + environment = Environment._get_python_env_info(file_name, python, requirements_file=requirements_file) assert environment.python_interpreter == expected_python assert environment == expected_environment diff --git a/tests/testdata/stock-api-fastapi/main.py b/tests/testdata/stock-api-fastapi/main.py index c69f2ca1..867f027a 100644 --- a/tests/testdata/stock-api-fastapi/main.py +++ b/tests/testdata/stock-api-fastapi/main.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import os -from datetime import date +from datetime import date # noqa: F401 from typing import List import numpy as np