diff --git a/docs/cli.md b/docs/cli.md index a4c682a757d..f09ffb97273 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -225,6 +225,21 @@ If you want to skip this installation, use the `--no-root` option. poetry install --no-root ``` +By default `poetry` does not compile Python source files to bytecode during installation. +This speeds up the installation process, but the first execution may take a little more +time because Python then compiles source files to bytecode automatically. +If you want to compile source files to bytecode during installation, +you can use the `--compile` option: + +```bash +poetry install --compile +``` + +{{% note %}} +The `--compile` option has no effect if `installer.modern-installation` +is set to `false` because the old installer always compiles source files to bytecode. +{{% /note %}} + ### Options * `--without`: The dependency groups to ignore. @@ -236,6 +251,7 @@ poetry install --no-root * `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). * `--extras (-E)`: Features to install (multiple values allowed). * `--all-extras`: Install all extra features (conflicts with --extras). +* `--compile`: Compile Python source files to bytecode. * `--no-dev`: Do not install dev dependencies. (**Deprecated**, use `--without dev` or `--only main` instead) * `--remove-untracked`: Remove dependencies not presented in the lock file. (**Deprecated**, use `--sync` instead) diff --git a/src/poetry/console/commands/install.py b/src/poetry/console/commands/install.py index b499970aac9..453feaf55d0 100644 --- a/src/poetry/console/commands/install.py +++ b/src/poetry/console/commands/install.py @@ -54,12 +54,15 @@ class InstallCommand(InstallerCommand): multiple=True, ), option("all-extras", None, "Install all extra dependencies."), + option("only-root", None, "Exclude all dependencies."), option( - "only-root", + "compile", None, - "Exclude all dependencies.", - flag=True, - multiple=False, + ( + "Compile Python source files to bytecode." + " (This option has no effect if modern-installation is disabled" + " because the old installer always compiles.)" + ), ), ] @@ -146,6 +149,7 @@ def handle(self) -> int: self.installer.only_groups(self.activated_groups) self.installer.dry_run(self.option("dry-run")) self.installer.requires_synchronization(with_synchronization) + self.installer.executor.enable_bytecode_compilation(self.option("compile")) self.installer.verbose(self.io.is_verbose()) return_code = self.installer.run() diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index 764f3dd6aab..6ba544eb40d 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -123,6 +123,9 @@ def verbose(self, verbose: bool = True) -> Executor: return self + def enable_bytecode_compilation(self, enable: bool = True) -> None: + self._wheel_installer.enable_bytecode_compilation(enable) + def pip_install( self, req: Path, upgrade: bool = False, editable: bool = False ) -> int: diff --git a/src/poetry/installation/wheel_installer.py b/src/poetry/installation/wheel_installer.py index 0611d763934..a2dcc01b66d 100644 --- a/src/poetry/installation/wheel_installer.py +++ b/src/poetry/installation/wheel_installer.py @@ -64,7 +64,10 @@ def for_source(self, source: WheelFile) -> WheelDestination: scheme_dict["headers"] = str(Path(scheme_dict["headers"]) / source.distribution) return self.__class__( - scheme_dict, interpreter=self.interpreter, script_kind=self.script_kind + scheme_dict, + interpreter=self.interpreter, + script_kind=self.script_kind, + bytecode_optimization_levels=self.bytecode_optimization_levels, ) @@ -88,6 +91,9 @@ def __init__(self, env: Env) -> None: schemes, interpreter=self._env.python, script_kind=script_kind ) + def enable_bytecode_compilation(self, enable: bool = True) -> None: + self._destination.bytecode_optimization_levels = (1,) if enable else () + def install(self, wheel: Path) -> None: with WheelFile.open(Path(wheel.as_posix())) as source: install( diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index 9e4e59d1b5e..0200a452bfa 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -162,6 +162,24 @@ def test_sync_option_is_passed_to_the_installer( assert tester.command.installer._requires_synchronization +@pytest.mark.parametrize("compile", [False, True]) +def test_compile_option_is_passed_to_the_installer( + tester: CommandTester, mocker: MockerFixture, compile: bool +): + """ + The --compile option is passed properly to the installer. + """ + mocker.patch.object(tester.command.installer, "run", return_value=1) + enable_bytecode_compilation_mock = mocker.patch.object( + tester.command.installer.executor._wheel_installer, + "enable_bytecode_compilation", + ) + + tester.execute("--compile" if compile else "") + + enable_bytecode_compilation_mock.assert_called_once_with(compile) + + def test_no_all_extras_doesnt_populate_installer( tester: CommandTester, mocker: MockerFixture ): diff --git a/tests/installation/test_wheel_installer.py b/tests/installation/test_wheel_installer.py index 42dcec76292..7fc4826f940 100644 --- a/tests/installation/test_wheel_installer.py +++ b/tests/installation/test_wheel_installer.py @@ -59,3 +59,23 @@ def test_installer_file_contains_valid_version(default_installation: Path) -> No match = re.match(r"Poetry (?P.*)", installer_content) assert match parse_constraint(match.group("version")) # must not raise an error + + +def test_default_installation_no_bytecode(default_installation: Path) -> None: + cache_dir = default_installation / "demo" / "__pycache__" + assert not cache_dir.exists() + + +@pytest.mark.parametrize("compile", [True, False]) +def test_enable_bytecode_compilation( + env: MockEnv, demo_wheel: Path, compile: bool +) -> None: + installer = WheelInstaller(env) + installer.enable_bytecode_compilation(compile) + installer.install(demo_wheel) + cache_dir = Path(env.paths["purelib"]) / "demo" / "__pycache__" + if compile: + assert cache_dir.exists() + assert list(cache_dir.glob("*.pyc")) + else: + assert not cache_dir.exists()