Commit cdbacd64 by Maximilian Committed by GitHub

Add maximum of workers to config (#3516)

parent 28591019
...@@ -38,3 +38,5 @@ MANIFEST.in ...@@ -38,3 +38,5 @@ MANIFEST.in
/releases/* /releases/*
pip-wheel-metadata pip-wheel-metadata
/poetry.toml /poetry.toml
poetry/core/*
...@@ -123,6 +123,21 @@ Defaults to one of the following directories: ...@@ -123,6 +123,21 @@ Defaults to one of the following directories:
Use parallel execution when using the new (`>=1.1.0`) installer. Use parallel execution when using the new (`>=1.1.0`) installer.
Defaults to `true`. Defaults to `true`.
### `installer.max-workers`
**Type**: int
Set the maximum number of workers while using the parallel installer. Defaults to `number_of_cores + 4`.
The `number_of_cores` is determined by `os.cpu_count()`.
If this raises a `NotImplentedError` exception `number_of_cores` is assumed to be 1.
If this configuration parameter is set to a value greater than `number_of_cores + 4`,
the number of maximum workers is still limited at `number_of_cores + 4`.
{{% note %}}
This configuration will be ignored when `installer.parallel` is set to false.
{{% /note %}}
### `virtualenvs.create` ### `virtualenvs.create`
**Type**: boolean **Type**: boolean
......
...@@ -212,6 +212,14 @@ docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>= ...@@ -212,6 +212,14 @@ docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=
testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
[[package]] [[package]]
name = "flatdict"
version = "4.0.1"
description = "Python module for interacting with nested dicts as a single level dict with delimited keys."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "html5lib" name = "html5lib"
version = "1.1" version = "1.1"
description = "HTML parser based on the WHATWG HTML specification" description = "HTML parser based on the WHATWG HTML specification"
...@@ -739,7 +747,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes ...@@ -739,7 +747,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.6" python-versions = "^3.6"
content-hash = "9d2e32899df46f2c63018e9a3f5e95dbbeb1ec41291c31289cff40f6f2d935a4" content-hash = "d427df125a868ada92bbb6d3a8cc90def6034ad684c1546afb519729048ab150"
[metadata.files] [metadata.files]
atomicwrites = [ atomicwrites = [
...@@ -929,6 +937,9 @@ filelock = [ ...@@ -929,6 +937,9 @@ filelock = [
{file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"}, {file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"},
{file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"}, {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"},
] ]
flatdict = [
{file = "flatdict-4.0.1.tar.gz", hash = "sha256:cd32f08fd31ed21eb09ebc76f06b6bd12046a24f77beb1fd0281917e47f26742"},
]
html5lib = [ html5lib = [
{file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"},
{file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"},
......
...@@ -65,6 +65,7 @@ deepdiff = "^5.0" ...@@ -65,6 +65,7 @@ deepdiff = "^5.0"
httpretty = "^1.0" httpretty = "^1.0"
typing-extensions = { version = "^4.0.0", python = "<3.8" } typing-extensions = { version = "^4.0.0", python = "<3.8" }
zipp = { version = "^3.4", python = "<3.8" } zipp = { version = "^3.4", python = "<3.8" }
flatdict = "^4.0.1"
[tool.poetry.scripts] [tool.poetry.scripts]
poetry = "poetry.console.application:main" poetry = "poetry.console.application:main"
......
...@@ -25,6 +25,10 @@ def boolean_normalizer(val: str) -> bool: ...@@ -25,6 +25,10 @@ def boolean_normalizer(val: str) -> bool:
return val in ["true", "1"] return val in ["true", "1"]
def int_normalizer(val: str) -> int:
return int(val)
class Config: class Config:
default_config: Dict[str, Any] = { default_config: Dict[str, Any] = {
...@@ -36,7 +40,7 @@ class Config: ...@@ -36,7 +40,7 @@ class Config:
"options": {"always-copy": False, "system-site-packages": False}, "options": {"always-copy": False, "system-site-packages": False},
}, },
"experimental": {"new-installer": True}, "experimental": {"new-installer": True},
"installer": {"parallel": True}, "installer": {"parallel": True, "max-workers": None},
} }
def __init__( def __init__(
...@@ -129,7 +133,8 @@ class Config: ...@@ -129,7 +133,8 @@ class Config:
return re.sub(r"{(.+?)}", lambda m: self.get(m.group(1)), value) return re.sub(r"{(.+?)}", lambda m: self.get(m.group(1)), value)
def _get_normalizer(self, name: str) -> Callable: @staticmethod
def _get_normalizer(name: str) -> Callable:
if name in { if name in {
"virtualenvs.create", "virtualenvs.create",
"virtualenvs.in-project", "virtualenvs.in-project",
...@@ -143,4 +148,7 @@ class Config: ...@@ -143,4 +148,7 @@ class Config:
if name == "virtualenvs.path": if name == "virtualenvs.path":
return lambda val: str(Path(val)) return lambda val: str(Path(val))
if name == "installer.max-workers":
return int_normalizer
return lambda val: val return lambda val: val
...@@ -52,6 +52,7 @@ To remove a repository (repo is a short alias for repositories): ...@@ -52,6 +52,7 @@ To remove a repository (repo is a short alias for repositories):
from poetry.config.config import boolean_normalizer from poetry.config.config import boolean_normalizer
from poetry.config.config import boolean_validator from poetry.config.config import boolean_validator
from poetry.config.config import int_normalizer
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
unique_config_values = { unique_config_values = {
...@@ -87,6 +88,11 @@ To remove a repository (repo is a short alias for repositories): ...@@ -87,6 +88,11 @@ To remove a repository (repo is a short alias for repositories):
boolean_normalizer, boolean_normalizer,
True, True,
), ),
"installer.max-workers": (
lambda val: int(val) > 0,
int_normalizer,
None,
),
} }
return unique_config_values return unique_config_values
......
...@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING ...@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional
from typing import Union from typing import Union
from cleo.io.null_io import NullIO from cleo.io.null_io import NullIO
...@@ -66,14 +67,9 @@ class Executor: ...@@ -66,14 +67,9 @@ class Executor:
parallel = config.get("installer.parallel", True) parallel = config.get("installer.parallel", True)
if parallel: if parallel:
# This should be directly handled by ThreadPoolExecutor self._max_workers = self._get_max_workers(
# however, on some systems the number of CPUs cannot be determined desired_max_workers=config.get("installer.max-workers")
# (it raises a NotImplementedError), so, in this case, we assume )
# that the system only has one CPU.
try:
self._max_workers = os.cpu_count() + 4
except NotImplementedError:
self._max_workers = 5
else: else:
self._max_workers = 1 self._max_workers = 1
...@@ -190,6 +186,21 @@ class Executor: ...@@ -190,6 +186,21 @@ class Executor:
return 1 if self._shutdown else 0 return 1 if self._shutdown else 0
@staticmethod
def _get_max_workers(desired_max_workers: Optional[int] = None):
# This should be directly handled by ThreadPoolExecutor
# however, on some systems the number of CPUs cannot be determined
# (it raises a NotImplementedError), so, in this case, we assume
# that the system only has one CPU.
try:
default_max_workers = os.cpu_count() + 4
except NotImplementedError:
default_max_workers = 5
if desired_max_workers is None:
return default_max_workers
return min(default_max_workers, desired_max_workers)
def _write(self, operation: "OperationTypes", line: str) -> None: def _write(self, operation: "OperationTypes", line: str) -> None:
if not self.supports_fancy_output() or not self._should_write_operation( if not self.supports_fancy_output() or not self._should_write_operation(
operation operation
......
...@@ -2,31 +2,29 @@ import os ...@@ -2,31 +2,29 @@ import os
import re import re
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Any from typing import Callable
from typing import Dict
from typing import Iterator from typing import Iterator
from typing import Optional
from typing import Tuple from typing import Tuple
import pytest import pytest
from flatdict import FlatDict
from poetry.config.config import Config from poetry.config.config import Config
from poetry.config.config import boolean_normalizer
from poetry.config.config import int_normalizer
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
def get_boolean_options(config: Optional[Dict[str, Any]] = None) -> str: def get_options_based_on_normalizer(normalizer: Callable) -> str:
if config is None: flattened_config = FlatDict(Config.default_config, delimiter=".")
config = Config.default_config
for k, v in config.items(): for k in flattened_config:
if isinstance(v, bool) or v is None: if Config._get_normalizer(k) == normalizer:
yield k yield k
if isinstance(v, dict):
for suboption in get_boolean_options(v):
yield f"{k}.{suboption}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
...@@ -43,9 +41,13 @@ def test_config_get_processes_depended_on_values( ...@@ -43,9 +41,13 @@ def test_config_get_processes_depended_on_values(
def generate_environment_variable_tests() -> Iterator[Tuple[str, str, str, bool]]: def generate_environment_variable_tests() -> Iterator[Tuple[str, str, str, bool]]:
for env_value, value in [("true", True), ("false", False)]: for normalizer, values in [
for name in get_boolean_options(): (boolean_normalizer, [("true", True), ("false", False)]),
env_var = "POETRY_{}".format(re.sub("[.-]+", "_", name).upper()) (int_normalizer, [("4", 4), ("2", 2)]),
]:
for env_value, value in values:
for name in get_options_based_on_normalizer(normalizer=normalizer):
env_var = "POETRY_" + re.sub("[.-]+", "_", name).upper()
yield name, env_var, env_value, value yield name, env_var, env_value, value
......
...@@ -46,6 +46,7 @@ def test_list_displays_default_value_if_not_set( ...@@ -46,6 +46,7 @@ def test_list_displays_default_value_if_not_set(
expected = """cache-dir = {cache} expected = """cache-dir = {cache}
experimental.new-installer = true experimental.new-installer = true
installer.max-workers = null
installer.parallel = true installer.parallel = true
virtualenvs.create = true virtualenvs.create = true
virtualenvs.in-project = null virtualenvs.in-project = null
...@@ -70,6 +71,7 @@ def test_list_displays_set_get_setting( ...@@ -70,6 +71,7 @@ def test_list_displays_set_get_setting(
expected = """cache-dir = {cache} expected = """cache-dir = {cache}
experimental.new-installer = true experimental.new-installer = true
installer.max-workers = null
installer.parallel = true installer.parallel = true
virtualenvs.create = false virtualenvs.create = false
virtualenvs.in-project = null virtualenvs.in-project = null
...@@ -118,6 +120,7 @@ def test_list_displays_set_get_local_setting( ...@@ -118,6 +120,7 @@ def test_list_displays_set_get_local_setting(
expected = """cache-dir = {cache} expected = """cache-dir = {cache}
experimental.new-installer = true experimental.new-installer = true
installer.max-workers = null
installer.parallel = true installer.parallel = true
virtualenvs.create = false virtualenvs.create = false
virtualenvs.in-project = null virtualenvs.in-project = null
......
...@@ -559,3 +559,35 @@ def test_executor_should_use_cached_link_and_hash( ...@@ -559,3 +559,35 @@ def test_executor_should_use_cached_link_and_hash(
Link("https://example.com/demo-0.1.0-py2.py3-none-any.whl"), Link("https://example.com/demo-0.1.0-py2.py3-none-any.whl"),
) )
assert archive == link_cached assert archive == link_cached
@pytest.mark.parametrize(
("max_workers", "cpu_count", "side_effect", "expected_workers"),
[
(None, 3, None, 7),
(3, 4, None, 3),
(8, 3, None, 7),
(None, 8, NotImplementedError(), 5),
(2, 8, NotImplementedError(), 2),
(8, 8, NotImplementedError(), 5),
],
)
def test_executor_should_be_initialized_with_correct_workers(
tmp_venv,
pool,
config,
io,
mocker,
max_workers,
cpu_count,
side_effect,
expected_workers,
):
config = Config()
config.merge({"installer": {"max-workers": max_workers}})
mocker.patch("os.cpu_count", return_value=cpu_count, side_effect=side_effect)
executor = Executor(tmp_venv, pool, config, io)
assert executor._max_workers == expected_workers
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment