Commit 01fddda5 by Sébastien Eustace Committed by GitHub

Improve the way settings are managed (#1272)

* Refactor configuration code

* Add support for local configuration

* Update documentation
parent f205ac75
...@@ -36,3 +36,4 @@ MANIFEST.in ...@@ -36,3 +36,4 @@ MANIFEST.in
.venv .venv
/releases/* /releases/*
pip-wheel-metadata pip-wheel-metadata
/poetry.toml
...@@ -10,26 +10,112 @@ This file can typically be found in one of the following directories: ...@@ -10,26 +10,112 @@ This file can typically be found in one of the following directories:
For Unix, we follow the XDG spec and support `$XDG_CONFIG_HOME`. For Unix, we follow the XDG spec and support `$XDG_CONFIG_HOME`.
That means, by default `~/.config/pypoetry`. That means, by default `~/.config/pypoetry`.
## Local configuration
Poetry also provides the ability to have settings that are specific to a project
by passing the `--local` option to the `config` command.
```bash
poetry config virtualenvs.create false --local
```
## Listing the current configuration
To list the current configuration you can use the `--list` option
of the `config` command:
```bash
poetry config --list
```
which will give you something similar to this:
```toml
cache-dir = "/path/to/cache/directory"
virtualenvs.create = true
virtualenvs.in-project = false
virtualenvs.path = "{cache-dir}/virtualenvs" # /path/to/cache/directory/virtualenvs
```
## Displaying a single configuration setting
If you want to see the value of a specific setting, you can
give its name to the `config` command
```bash
poetry config virtualenvs.path
```
For a full list of the supported settings see [Available settings](#available-settings).
## Adding or updating a configuration setting
To change or otherwise add a new configuration setting, you can pass
a value after the setting's name:
```bash
poetry config virtualenvs.path /path/to/cache/directory/virtualenvs
```
For a full list of the supported settings see [Available settings](#available-settings).
## Removing a specific setting
If you want to remove a previously set setting, you can use the `--unset` option:
```bash
poetry config virtualenvs.path --unset
```
The setting will then retrieve its default value.
## Using environment variables
Sometimes, in particular when using Poetry with CI tools, it's easier
to use environment variables and not have to execute configuration commands.
Poetry supports this and any setting can be set by using environment variables.
The environment variables must be prefixed by `POETRY_` and are comprised of the uppercase
name of the setting and with dots and dashes replaced by underscore, here is an example:
```bash
export POETRY_VIRTUALENVS_PATH=/path/to/virtualenvs/directory
```
This also works for secret settings, like credentials:
```bash
export POETRY_HTTP_BASIC_MY_REPOSITORY_PASSWORD=secret
```
## Available settings ## Available settings
### `settings.virtualenvs.create`: boolean ### `cache-dir`: string
The path to the cache directory used by Poetry.
Defaults to one of the following directories:
- macOS: `~/Library/Caches/pypoetry`
- Windows: `C:\Users\<username>\AppData\Local\pypoetry\Cache`
- Unix: `~/.cache/pypoetry/virtualenvs`
### `virtualenvs.create`: boolean
Create a new virtualenv if one doesn't already exist. Create a new virtualenv if one doesn't already exist.
Defaults to `true`. Defaults to `true`.
### `settings.virtualenvs.in-project`: boolean ### `virtualenvs.in-project`: boolean
Create the virtualenv inside the project's root directory. Create the virtualenv inside the project's root directory.
Defaults to `false`. Defaults to `false`.
### `settings.virtualenvs.path`: string ### `virtualenvs.path`: string
Directory where virtualenvs will be created. Directory where virtualenvs will be created.
Defaults to one of the following directories: Defaults to `{cache-dir}/virtualenvs`
- macOS: `~/Library/Caches/pypoetry/virtualenvs`
- Windows: `C:\Users\<username>\AppData\Local\pypoetry\Cache/virtualenvs`
- Unix: `~/.cache/pypoetry/virtualenvs`
### `repositories.<name>`: string ### `repositories.<name>`: string
......
...@@ -49,10 +49,20 @@ If you do not specify the password you will be prompted to write it. ...@@ -49,10 +49,20 @@ If you do not specify the password you will be prompted to write it.
You can also specify the username and password when using the `publish` command You can also specify the username and password when using the `publish` command
with the `--username` and `--password` options. with the `--username` and `--password` options.
If a system keyring is available and supported, the password is stored to and retrieved from the keyring. In the above example, the credential will be stored using the name `poetry-repository-pypi`. If access to keyring fails or is unsupported, this will fall back to writing the password to the `auth.toml` file along with the username. If a system keyring is available and supported, the password is stored to and retrieved from the keyring. In the above example, the credential will be stored using the name `poetry-repository-pypi`. If access to keyring fails or is unsupported, this will fall back to writing the password to the `auth.toml` file along with the username.
Keyring support is enabled using the [keyring library](https://pypi.org/project/keyring/). For more information on supported backends refer to the [library documentation](https://keyring.readthedocs.io/en/latest/?badge=latest). Keyring support is enabled using the [keyring library](https://pypi.org/project/keyring/). For more information on supported backends refer to the [library documentation](https://keyring.readthedocs.io/en/latest/?badge=latest).
Alternatively, you can use environment variables to provide the credentials:
```bash
export POETRY_HTTP_BASIC_PYPI_USERNAME=username
export POETRY_HTTP_BASIC_PYPI_PASSWORD=password
```
See [Using environment variables](/configuration#using-environment-variables) for more information
on how to configure Poetry with environment variables.
### Install dependencies from a private repository ### Install dependencies from a private repository
Now that you can publish to your private repository, you need to be able to Now that you can publish to your private repository, you need to be able to
...@@ -71,10 +81,10 @@ From now on, Poetry will also look for packages in your private repository. ...@@ -71,10 +81,10 @@ From now on, Poetry will also look for packages in your private repository.
!!!note !!!note
Any custom repository will have precedence over PyPI. Any custom repository will have precedence over PyPI.
If you still want PyPI to be your primary source for your packages If you still want PyPI to be your primary source for your packages
you can declare custom repositories as secondary. you can declare custom repositories as secondary.
```toml ```toml
[[tool.poetry.source]] [[tool.poetry.source]]
name = "foo" name = "foo"
......
from __future__ import absolute_import
import io
import os
from typing import Any
from tomlkit import document
from tomlkit import table
from .locations import CONFIG_DIR
from .utils._compat import Path
from .utils.toml_file import TomlFile
class Config:
def __init__(self, file): # type: (TomlFile) -> None
self._file = file
if not self._file.exists():
self._content = document()
else:
self._content = file.read()
@property
def name(self):
return str(self._file.path)
@property
def file(self):
return self._file
@property
def content(self):
return self._content
def setting(
self,
setting_name, # type: str
default=None,
):
"""
Retrieve a setting value.
"""
keys = setting_name.split(".")
config = self._content
for key in keys:
if key not in config:
return default
config = config[key]
return config
def add_property(self, key, value):
keys = key.split(".")
config = self._content
for i, key in enumerate(keys):
if key not in config and i < len(keys) - 1:
config[key] = table()
if i == len(keys) - 1:
config[key] = value
break
config = config[key]
self.dump()
def remove_property(self, key):
keys = key.split(".")
config = self._content
for i, key in enumerate(keys):
if key not in config:
return
if i == len(keys) - 1:
del config[key]
break
config = config[key]
self.dump()
def dump(self):
# Ensuring the file is only readable and writable
# by the current user
mode = 0o600
umask = 0o777 ^ mode
if self._file.exists():
# If the file already exists, remove it
# if the permissions are higher than what we want
current_mode = os.stat(str(self._file)).st_mode & 0o777
if current_mode != 384:
os.remove(str(self._file))
if self._file.exists():
fd = str(self._file)
else:
umask_original = os.umask(umask)
try:
fd = os.open(str(self._file), os.O_WRONLY | os.O_CREAT, mode)
finally:
os.umask(umask_original)
with io.open(fd, "w", encoding="utf-8") as f:
f.write(self._content.as_string())
@classmethod
def create(cls, file, base_dir=None): # type: (...) -> Config
if base_dir is None:
base_dir = CONFIG_DIR
file = TomlFile(Path(base_dir) / file)
return cls(file)
from __future__ import absolute_import
import os
import re
from copy import deepcopy
from typing import Any
from typing import Dict
from typing import Optional
from poetry.locations import CACHE_DIR
from poetry.utils._compat import Path
from poetry.utils._compat import basestring
_NOT_SET = object()
boolean_validator = lambda val: val in {"true", "false", "1", "0"}
boolean_normalizer = lambda val: val in ["true", "1"]
class Config:
default_config = {
"cache-dir": str(CACHE_DIR),
"virtualenvs": {
"create": True,
"in-project": False,
"path": os.path.join("{cache-dir}", "virtualenvs"),
},
}
def __init__(
self, use_environment=True, base_dir=None
): # type: (bool, Optional[Path]) -> None
self._config = deepcopy(self.default_config)
self._use_environment = use_environment
self._base_dir = base_dir
@property
def name(self):
return str(self._file.path)
@property
def config(self):
return self._config
def merge(self, config): # type: (Dict[str, Any]) -> None
from poetry.utils.helpers import merge_dicts
merge_dicts(self._config, config)
def all(self): # type: () -> Dict[str, Any]
def _all(config, parent_key=""):
all_ = {}
for key in config:
value = self.get(parent_key + key)
if isinstance(value, dict):
all_[key] = _all(config[key], parent_key=key + ".")
continue
all_[key] = value
return all_
return _all(self.config)
def raw(self): # type: () -> Dict[str, Any]
return self._config
def get(self, setting_name, default=None): # type: (str, Any) -> Any
"""
Retrieve a setting value.
"""
keys = setting_name.split(".")
# Looking in the environment if the setting
# is set via a POETRY_* environment variable
if self._use_environment:
env = "POETRY_{}".format(
"_".join(k.upper().replace("-", "_") for k in keys)
)
value = os.getenv(env, _NOT_SET)
if value is not _NOT_SET:
return self.process(self._get_normalizer(setting_name)(value))
value = self._config
for key in keys:
if key not in value:
return self.process(default)
value = value[key]
return self.process(value)
def process(self, value): # type: (Any) -> Any
if not isinstance(value, basestring):
return value
return re.sub(r"{(.+?)}", lambda m: self.get(m.group(1)), value)
def _get_validator(self, name): # type: (str) -> Callable
if name in {"virtualenvs.create", "virtualenvs.in-project"}:
return boolean_validator
if name == "virtualenvs.path":
return str
def _get_normalizer(self, name): # type: (str) -> Callable
if name in {"virtualenvs.create", "virtualenvs.in-project"}:
return boolean_normalizer
if name == "virtualenvs.path":
return lambda val: str(Path(val))
return lambda val: val
import io
import os
from contextlib import contextmanager
from typing import Any
from tomlkit import document
from tomlkit import table
from poetry.utils.toml_file import TomlFile
class ConfigSource:
def __init__(self, file, auth_config=False): # type: (TomlFile, bool) -> None
self._file = file
self._auth_config = auth_config
@property
def name(self): # type: () -> str
return str(self._file.path)
@property
def file(self): # type: () -> TomlFile
return self._file
def add_property(self, key, value): # type: (str, Any) -> None
with self.secure() as config:
keys = key.split(".")
for i, key in enumerate(keys):
if key not in config and i < len(keys) - 1:
config[key] = table()
if i == len(keys) - 1:
config[key] = value
break
config = config[key]
def remove_property(self, key): # type: (str) -> None
with self.secure() as config:
keys = key.split(".")
current_config = config
for i, key in enumerate(keys):
if key not in current_config:
return
if i == len(keys) - 1:
del current_config[key]
break
current_config = current_config[key]
@contextmanager
def secure(self):
if self.file.exists():
initial_config = self.file.read()
config = self.file.read()
else:
initial_config = document()
config = document()
new_file = not self.file.exists()
yield config
try:
# Ensuring the file is only readable and writable
# by the current user
mode = 0o600
if new_file:
self.file.touch(mode=mode)
self.file.write(config)
except Exception:
self.file.write(initial_config)
raise
...@@ -10,14 +10,6 @@ from poetry.utils.helpers import ( ...@@ -10,14 +10,6 @@ from poetry.utils.helpers import (
) )
from .command import Command from .command import Command
TEMPLATE = """[settings]
[repositories]
"""
AUTH_TEMPLATE = """[http-basic]
"""
class ConfigCommand(Command): class ConfigCommand(Command):
...@@ -32,6 +24,7 @@ class ConfigCommand(Command): ...@@ -32,6 +24,7 @@ class ConfigCommand(Command):
options = [ options = [
option("list", None, "List configuration settings."), option("list", None, "List configuration settings."),
option("unset", None, "Unset configuration setting."), option("unset", None, "Unset configuration setting."),
option("local", None, "Set/Get from the project's local configuration."),
] ]
help = """This command allows you to edit the poetry config settings and repositories. help = """This command allows you to edit the poetry config settings and repositories.
...@@ -42,16 +35,7 @@ To add a repository: ...@@ -42,16 +35,7 @@ To add a repository:
To remove a repository (repo is a short alias for repositories): To remove a repository (repo is a short alias for repositories):
<comment>poetry config --unset repo.foo</comment> <comment>poetry config --unset repo.foo</comment>"""
"""
def __init__(self):
from poetry.config import Config
super(ConfigCommand, self).__init__()
self._settings_config = Config.create("config.toml")
self._auth_config = Config.create("auth.toml")
@property @property
def unique_config_values(self): def unique_config_values(self):
...@@ -59,20 +43,17 @@ To remove a repository (repo is a short alias for repositories): ...@@ -59,20 +43,17 @@ To remove a repository (repo is a short alias for repositories):
from poetry.utils._compat import Path from poetry.utils._compat import Path
boolean_validator = lambda val: val in {"true", "false", "1", "0"} boolean_validator = lambda val: val in {"true", "false", "1", "0"}
boolean_normalizer = lambda val: True if val in ["true", "1"] else False boolean_normalizer = lambda val: val in ["true", "1"]
unique_config_values = { unique_config_values = {
"settings.virtualenvs.create": ( "cache-dir": (
boolean_validator, str,
boolean_normalizer, lambda val: str(Path(val)),
True, str(Path(CACHE_DIR) / "virtualenvs"),
),
"settings.virtualenvs.in-project": (
boolean_validator,
boolean_normalizer,
False,
), ),
"settings.virtualenvs.path": ( "virtualenvs.create": (boolean_validator, boolean_normalizer, True),
"virtualenvs.in-project": (boolean_validator, boolean_normalizer, False),
"virtualenvs.path": (
str, str,
lambda val: str(Path(val)), lambda val: str(Path(val)),
str(Path(CACHE_DIR) / "virtualenvs"), str(Path(CACHE_DIR) / "virtualenvs"),
...@@ -81,23 +62,35 @@ To remove a repository (repo is a short alias for repositories): ...@@ -81,23 +62,35 @@ To remove a repository (repo is a short alias for repositories):
return unique_config_values return unique_config_values
def initialize(self, i, o): def handle(self):
from poetry.utils._compat import decode from poetry.config.config import Config
from poetry.config.config_source import ConfigSource
from poetry.locations import CONFIG_DIR
from poetry.utils._compat import Path
from poetry.utils._compat import basestring
from poetry.utils.toml_file import TomlFile
# Create config file if it does not exist config = Config()
if not self._settings_config.file.exists(): config_file = TomlFile(Path(CONFIG_DIR) / "config.toml")
self._settings_config.file.parent.mkdir(parents=True, exist_ok=True) config_source = ConfigSource(config_file)
with self._settings_config.file.open("w", encoding="utf-8") as f: config.merge(config_source.file.read())
f.write(decode(TEMPLATE))
if not self._auth_config.file.exists(): auth_config_file = TomlFile(Path(CONFIG_DIR) / "auth.toml")
self._auth_config.file.parent.mkdir(parents=True, exist_ok=True) auth_config_source = ConfigSource(auth_config_file, auth_config=True)
with self._auth_config.file.open("w", encoding="utf-8") as f:
f.write(decode(AUTH_TEMPLATE)) local_config_file = TomlFile(self.poetry.file.parent / "poetry.toml")
if local_config_file.exists():
config.merge(local_config_file.read())
if self.option("local"):
config_source = ConfigSource(local_config_file)
if not config_file.exists():
config_file.path.parent.mkdir(parents=True, exist_ok=True)
config_file.touch(mode=0o0600)
def handle(self):
if self.option("list"): if self.option("list"):
self._list_configuration(self._settings_config.content) self._list_configuration(config.all(), config.raw())
return 0 return 0
...@@ -114,12 +107,10 @@ To remove a repository (repo is a short alias for repositories): ...@@ -114,12 +107,10 @@ To remove a repository (repo is a short alias for repositories):
if m: if m:
if not m.group(1): if not m.group(1):
value = {} value = {}
if self._settings_config.setting("repositories") is not None: if config.get("repositories") is not None:
value = self._settings_config.setting("repositories") value = config.get("repositories")
else: else:
repo = self._settings_config.setting( repo = config.get("repositories.{}".format(m.group(1)))
"repositories.{}".format(m.group(1))
)
if repo is None: if repo is None:
raise ValueError( raise ValueError(
"There is no {} repository defined".format(m.group(1)) "There is no {} repository defined".format(m.group(1))
...@@ -133,14 +124,12 @@ To remove a repository (repo is a short alias for repositories): ...@@ -133,14 +124,12 @@ To remove a repository (repo is a short alias for repositories):
if setting_key not in values: if setting_key not in values:
raise ValueError("There is no {} setting.".format(setting_key)) raise ValueError("There is no {} setting.".format(setting_key))
values = self._get_setting( value = config.get(setting_key)
self._settings_config.content,
setting_key, if not isinstance(value, basestring):
default=values[setting_key][-1], value = json.dumps(value)
)
for value in values: self.line(value)
self.line(value[1])
return 0 return 0
...@@ -149,10 +138,10 @@ To remove a repository (repo is a short alias for repositories): ...@@ -149,10 +138,10 @@ To remove a repository (repo is a short alias for repositories):
unique_config_values = self.unique_config_values unique_config_values = self.unique_config_values
if setting_key in unique_config_values: if setting_key in unique_config_values:
if self.option("unset"): if self.option("unset"):
return self._remove_single_value(setting_key) return config_source.remove_property(setting_key)
return self._handle_single_value( return self._handle_single_value(
setting_key, unique_config_values[setting_key], values config_source, setting_key, unique_config_values[setting_key], values
) )
# handle repositories # handle repositories
...@@ -162,24 +151,20 @@ To remove a repository (repo is a short alias for repositories): ...@@ -162,24 +151,20 @@ To remove a repository (repo is a short alias for repositories):
raise ValueError("You cannot remove the [repositories] section") raise ValueError("You cannot remove the [repositories] section")
if self.option("unset"): if self.option("unset"):
repo = self._settings_config.setting( repo = config.get("repositories.{}".format(m.group(1)))
"repositories.{}".format(m.group(1))
)
if repo is None: if repo is None:
raise ValueError( raise ValueError(
"There is no {} repository defined".format(m.group(1)) "There is no {} repository defined".format(m.group(1))
) )
self._settings_config.remove_property( config_source.remove_property("repositories.{}".format(m.group(1)))
"repositories.{}".format(m.group(1))
)
return 0 return 0
if len(values) == 1: if len(values) == 1:
url = values[0] url = values[0]
self._settings_config.add_property( config_source.add_property(
"repositories.{}.url".format(m.group(1)), url "repositories.{}.url".format(m.group(1)), url
) )
...@@ -194,15 +179,8 @@ To remove a repository (repo is a short alias for repositories): ...@@ -194,15 +179,8 @@ To remove a repository (repo is a short alias for repositories):
m = re.match(r"^(http-basic)\.(.+)", self.argument("key")) m = re.match(r"^(http-basic)\.(.+)", self.argument("key"))
if m: if m:
if self.option("unset"): if self.option("unset"):
if not self._auth_config.setting( keyring_repository_password_del(config, m.group(2))
"{}.{}".format(m.group(1), m.group(2)) auth_config_source.remove_property(
):
raise ValueError(
"There is no {} {} defined".format(m.group(2), m.group(1))
)
keyring_repository_password_del(self._auth_config, m.group(2))
self._auth_config.remove_property(
"{}.{}".format(m.group(1), m.group(2)) "{}.{}".format(m.group(1), m.group(2))
) )
...@@ -228,7 +206,7 @@ To remove a repository (repo is a short alias for repositories): ...@@ -228,7 +206,7 @@ To remove a repository (repo is a short alias for repositories):
except RuntimeError: except RuntimeError:
property_value.update(password=password) property_value.update(password=password)
self._auth_config.add_property( auth_config_source.add_property(
"{}.{}".format(m.group(1), m.group(2)), property_value "{}.{}".format(m.group(1), m.group(2)), property_value
) )
...@@ -236,7 +214,7 @@ To remove a repository (repo is a short alias for repositories): ...@@ -236,7 +214,7 @@ To remove a repository (repo is a short alias for repositories):
raise ValueError("Setting {} does not exist".format(self.argument("key"))) raise ValueError("Setting {} does not exist".format(self.argument("key")))
def _handle_single_value(self, key, callbacks, values): def _handle_single_value(self, source, key, callbacks, values):
validator, normalizer, _ = callbacks validator, normalizer, _ = callbacks
if len(values) > 1: if len(values) > 1:
...@@ -246,33 +224,42 @@ To remove a repository (repo is a short alias for repositories): ...@@ -246,33 +224,42 @@ To remove a repository (repo is a short alias for repositories):
if not validator(value): if not validator(value):
raise RuntimeError('"{}" is an invalid value for {}'.format(value, key)) raise RuntimeError('"{}" is an invalid value for {}'.format(value, key))
self._settings_config.add_property(key, normalizer(value)) source.add_property(key, normalizer(value))
return 0 return 0
def _remove_single_value(self, key): def _list_configuration(self, config, raw, k=""):
self._settings_config.remove_property(key) from poetry.utils._compat import basestring
return 0 orig_k = k
for key, value in sorted(config.items()):
raw_val = raw.get(key)
def _list_configuration(self, contents): if isinstance(value, dict):
if "settings" not in contents: k += "{}.".format(key)
settings = {} self._list_configuration(value, raw_val, k=k)
else: k = orig_k
settings = contents["settings"]
for setting_key, value in sorted(self.unique_config_values.items()):
self._list_setting(
settings,
setting_key.replace("settings.", ""),
"settings.",
default=value[-1],
)
repositories = contents.get("repositories") continue
if not repositories: elif isinstance(value, list):
self.line("<comment>repositories</comment> = <info>{}</info>") value = [
else: json.dumps(val) if isinstance(val, list) else val for val in value
self._list_setting(repositories, k="repositories.") ]
value = "[{}]".format(", ".join(value))
if k.startswith("repositories."):
message = "<c1>{}</c1> = <c2>{}</c2>".format(
k + key, json.dumps(raw_val)
)
elif isinstance(raw_val, basestring) and raw_val != value:
message = "<c1>{}</c1> = <c2>{}</c2> # {}".format(
k + key, json.dumps(raw_val), value
)
else:
message = "<c1>{}</c1> = <c2>{}</c2>".format(k + key, json.dumps(value))
self.line(message)
def _list_setting(self, contents, setting=None, k=None, default=None): def _list_setting(self, contents, setting=None, k=None, default=None):
values = self._get_setting(contents, setting, k, default) values = self._get_setting(contents, setting, k, default)
...@@ -292,9 +279,6 @@ To remove a repository (repo is a short alias for repositories): ...@@ -292,9 +279,6 @@ To remove a repository (repo is a short alias for repositories):
else: else:
values = [] values = []
for key, value in contents.items(): for key, value in contents.items():
if k is None and key not in ["config", "repositories", "settings"]:
continue
if setting and key != setting.split(".")[0]: if setting and key != setting.split(".")[0]:
continue continue
......
...@@ -44,28 +44,18 @@ class Publisher: ...@@ -44,28 +44,18 @@ class Publisher:
repository_name = "pypi" repository_name = "pypi"
else: else:
# Retrieving config information # Retrieving config information
config_file = TomlFile(Path(CONFIG_DIR) / "config.toml") repository = self._poetry.config.get(
"repositories.{}".format(repository_name)
if not config_file.exists(): )
raise RuntimeError( if repository is None:
"Config file does not exist. "
"Unable to get repository information"
)
config = config_file.read()
if (
"repositories" not in config
or repository_name not in config["repositories"]
):
raise RuntimeError( raise RuntimeError(
"Repository {} is not defined".format(repository_name) "Repository {} is not defined".format(repository_name)
) )
url = config["repositories"][repository_name]["url"] url = repository["url"]
if not (username and password): if not (username and password):
auth = get_http_basic_auth(self._poetry.auth_config, repository_name) auth = get_http_basic_auth(self._poetry.config, repository_name)
if auth: if auth:
username = auth[0] username = auth[0]
password = auth[1] password = auth[1]
......
...@@ -7,8 +7,9 @@ from typing import Dict ...@@ -7,8 +7,9 @@ from typing import Dict
from typing import List from typing import List
from .__version__ import __version__ from .__version__ import __version__
from .config import Config from .config.config import Config
from .json import validate_object from .json import validate_object
from .locations import CONFIG_DIR
from .packages import Dependency from .packages import Dependency
from .packages import Locker from .packages import Locker
from .packages import Package from .packages import Package
...@@ -33,13 +34,13 @@ class Poetry: ...@@ -33,13 +34,13 @@ class Poetry:
local_config, # type: dict local_config, # type: dict
package, # type: Package package, # type: Package
locker, # type: Locker locker, # type: Locker
config, # type: Config
): ):
self._file = TomlFile(file) self._file = TomlFile(file)
self._package = package self._package = package
self._local_config = local_config self._local_config = local_config
self._locker = locker self._locker = locker
self._config = Config.create("config.toml") self._config = config
self._auth_config = Config.create("auth.toml")
# Configure sources # Configure sources
self._pool = Pool() self._pool = Pool()
...@@ -80,10 +81,6 @@ class Poetry: ...@@ -80,10 +81,6 @@ class Poetry:
def config(self): # type: () -> Config def config(self): # type: () -> Config
return self._config return self._config
@property
def auth_config(self): # type: () -> Config
return self._auth_config
@classmethod @classmethod
def create(cls, cwd): # type: (Path) -> Poetry def create(cls, cwd): # type: (Path) -> Poetry
poetry_file = cls.locate(cwd) poetry_file = cls.locate(cwd)
...@@ -200,7 +197,22 @@ class Poetry: ...@@ -200,7 +197,22 @@ class Poetry:
locker = Locker(poetry_file.parent / "poetry.lock", local_config) locker = Locker(poetry_file.parent / "poetry.lock", local_config)
return cls(poetry_file, local_config, package, locker) config = Config()
# Load global config
config_file = TomlFile(Path(CONFIG_DIR) / "config.toml")
if config_file.exists():
config.merge(config_file.read())
local_config_file = TomlFile(poetry_file.parent / "poetry.toml")
if local_config_file.exists():
config.merge(local_config_file.read())
# Load global auth config
auth_config_file = TomlFile(Path(CONFIG_DIR) / "auth.toml")
if auth_config_file.exists():
config.merge(auth_config_file.read())
return cls(poetry_file, local_config, package, locker, config)
def create_legacy_repository( def create_legacy_repository(
self, source self, source
...@@ -214,7 +226,7 @@ class Poetry: ...@@ -214,7 +226,7 @@ class Poetry:
name = source["name"] name = source["name"]
url = source["url"] url = source["url"]
credentials = get_http_basic_auth(self._auth_config, name) credentials = get_http_basic_auth(self._config, name)
if not credentials: if not credentials:
return LegacyRepository(name, url) return LegacyRepository(name, url)
......
...@@ -20,7 +20,7 @@ from typing import Tuple ...@@ -20,7 +20,7 @@ from typing import Tuple
from clikit.api.io import IO from clikit.api.io import IO
from poetry.config import Config from poetry.config.config import Config
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
from poetry.semver.version import Version from poetry.semver.version import Version
from poetry.utils._compat import CalledProcessError from poetry.utils._compat import CalledProcessError
...@@ -138,12 +138,12 @@ class EnvManager(object): ...@@ -138,12 +138,12 @@ class EnvManager(object):
def __init__(self, config=None): # type: (Config) -> None def __init__(self, config=None): # type: (Config) -> None
if config is None: if config is None:
config = Config.create("config.toml") config = Config()
self._config = config self._config = config
def activate(self, python, cwd, io): # type: (str, Optional[Path], IO) -> Env def activate(self, python, cwd, io): # type: (str, Optional[Path], IO) -> Env
venv_path = self._config.setting("settings.virtualenvs.path") venv_path = self._config.get("virtualenvs.path")
if venv_path is None: if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs" venv_path = Path(CACHE_DIR) / "virtualenvs"
else: else:
...@@ -220,7 +220,7 @@ class EnvManager(object): ...@@ -220,7 +220,7 @@ class EnvManager(object):
return self.get(cwd, reload=True) return self.get(cwd, reload=True)
def deactivate(self, cwd, io): # type: (Optional[Path], IO) -> None def deactivate(self, cwd, io): # type: (Optional[Path], IO) -> None
venv_path = self._config.setting("settings.virtualenvs.path") venv_path = self._config.get("virtualenvs.path")
if venv_path is None: if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs" venv_path = Path(CACHE_DIR) / "virtualenvs"
else: else:
...@@ -249,7 +249,7 @@ class EnvManager(object): ...@@ -249,7 +249,7 @@ class EnvManager(object):
python_minor = ".".join([str(v) for v in sys.version_info[:2]]) python_minor = ".".join([str(v) for v in sys.version_info[:2]])
venv_path = self._config.setting("settings.virtualenvs.path") venv_path = self._config.get("virtualenvs.path")
if venv_path is None: if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs" venv_path = Path(CACHE_DIR) / "virtualenvs"
else: else:
...@@ -274,12 +274,12 @@ class EnvManager(object): ...@@ -274,12 +274,12 @@ class EnvManager(object):
return VirtualEnv(venv) return VirtualEnv(venv)
create_venv = self._config.setting("settings.virtualenvs.create", True) create_venv = self._config.get("virtualenvs.create", True)
if not create_venv: if not create_venv:
return SystemEnv(Path(sys.prefix)) return SystemEnv(Path(sys.prefix))
venv_path = self._config.setting("settings.virtualenvs.path") venv_path = self._config.get("virtualenvs.path")
if venv_path is None: if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs" venv_path = Path(CACHE_DIR) / "virtualenvs"
else: else:
...@@ -309,7 +309,7 @@ class EnvManager(object): ...@@ -309,7 +309,7 @@ class EnvManager(object):
venv_name = self.generate_env_name(name, str(cwd)) venv_name = self.generate_env_name(name, str(cwd))
venv_path = self._config.setting("settings.virtualenvs.path") venv_path = self._config.get("virtualenvs.path")
if venv_path is None: if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs" venv_path = Path(CACHE_DIR) / "virtualenvs"
else: else:
...@@ -321,7 +321,7 @@ class EnvManager(object): ...@@ -321,7 +321,7 @@ class EnvManager(object):
] ]
def remove(self, python, cwd): # type: (str, Optional[Path]) -> Env def remove(self, python, cwd): # type: (str, Optional[Path]) -> Env
venv_path = self._config.setting("settings.virtualenvs.path") venv_path = self._config.get("virtualenvs.path")
if venv_path is None: if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs" venv_path = Path(CACHE_DIR) / "virtualenvs"
else: else:
...@@ -423,10 +423,10 @@ class EnvManager(object): ...@@ -423,10 +423,10 @@ class EnvManager(object):
# Already inside a virtualenv. # Already inside a virtualenv.
return env return env
create_venv = self._config.setting("settings.virtualenvs.create") create_venv = self._config.get("virtualenvs.create")
root_venv = self._config.setting("settings.virtualenvs.in-project") root_venv = self._config.get("virtualenvs.in-project")
venv_path = self._config.setting("settings.virtualenvs.path") venv_path = self._config.get("virtualenvs.path")
if root_venv: if root_venv:
venv_path = cwd / ".venv" venv_path = cwd / ".venv"
elif venv_path is None: elif venv_path is None:
......
import collections
import os import os
import re import re
import shutil import shutil
...@@ -12,7 +13,7 @@ from typing import Optional ...@@ -12,7 +13,7 @@ from typing import Optional
from keyring import delete_password, set_password, get_password from keyring import delete_password, set_password, get_password
from keyring.errors import KeyringError from keyring.errors import KeyringError
from poetry.config import Config from poetry.config.config import Config
from poetry.version import Version from poetry.version import Version
_canonicalize_regex = re.compile("[-_]+") _canonicalize_regex = re.compile("[-_]+")
...@@ -112,7 +113,7 @@ def keyring_repository_password_del( ...@@ -112,7 +113,7 @@ def keyring_repository_password_del(
config, repository_name config, repository_name
): # type: (Config, str) -> NoReturn ): # type: (Config, str) -> NoReturn
try: try:
repo_auth = config.setting("http-basic.{}".format(repository_name)) repo_auth = config.get("http-basic.{}".format(repository_name))
if repo_auth and "username" in repo_auth: if repo_auth and "username" in repo_auth:
delete_password( delete_password(
keyring_service_name(repository_name), repo_auth["username"] keyring_service_name(repository_name), repo_auth["username"]
...@@ -124,7 +125,7 @@ def keyring_repository_password_del( ...@@ -124,7 +125,7 @@ def keyring_repository_password_del(
def get_http_basic_auth( def get_http_basic_auth(
config, repository_name config, repository_name
): # type: (Config, str) -> Optional[tuple] ): # type: (Config, str) -> Optional[tuple]
repo_auth = config.setting("http-basic.{}".format(repository_name)) repo_auth = config.get("http-basic.{}".format(repository_name))
if repo_auth: if repo_auth:
username, password = repo_auth["username"], repo_auth.get("password") username, password = repo_auth["username"], repo_auth.get("password")
if password is None: if password is None:
...@@ -140,3 +141,15 @@ def _on_rm_error(func, path, exc_info): ...@@ -140,3 +141,15 @@ def _on_rm_error(func, path, exc_info):
def safe_rmtree(path): def safe_rmtree(path):
shutil.rmtree(path, onerror=_on_rm_error) shutil.rmtree(path, onerror=_on_rm_error)
def merge_dicts(d1, d2):
for k, v in d2.items():
if (
k in d1
and isinstance(d1[k], dict)
and isinstance(d2[k], collections.Mapping)
):
merge_dicts(d1[k], d2[k])
else:
d1[k] = d2[k]
...@@ -15,6 +15,9 @@ class TomlFile(BaseTOMLFile): ...@@ -15,6 +15,9 @@ class TomlFile(BaseTOMLFile):
def path(self): # type: () -> Path def path(self): # type: () -> Path
return self._path_ return self._path_
def exists(self): # type: () -> bool
return self._path_.exists()
def __getattr__(self, item): def __getattr__(self, item):
return getattr(self._path_, item) return getattr(self._path_, item)
......
import os
from poetry.locations import CACHE_DIR
def test_config_get_default_value(config):
assert config.get("virtualenvs.create") is True
def test_config_get_processes_depended_on_values(config):
assert os.path.join(str(CACHE_DIR), "virtualenvs") == config.get("virtualenvs.path")
def test_config_get_from_environment_variable(config, environ):
assert config.get("virtualenvs.create")
os.environ["POETRY_VIRTUALENVS_CREATE"] = "false"
assert not config.get("virtualenvs.create")
...@@ -9,10 +9,13 @@ try: ...@@ -9,10 +9,13 @@ try:
except ImportError: except ImportError:
import urlparse import urlparse
from poetry.config import Config from tomlkit import parse
from poetry.config.config import Config
from poetry.utils._compat import PY2 from poetry.utils._compat import PY2
from poetry.utils._compat import WINDOWS from poetry.utils._compat import WINDOWS
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.helpers import merge_dicts
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
...@@ -26,11 +29,34 @@ def tmp_dir(): ...@@ -26,11 +29,34 @@ def tmp_dir():
@pytest.fixture @pytest.fixture
def config(): # type: () -> Config def config_document():
with tempfile.NamedTemporaryFile() as f: content = """cache-dir = "/foo"
f.close() """
doc = parse(content)
return doc
@pytest.fixture
def config_source(config_document, mocker):
file = TomlFile(Path(tempfile.mktemp()))
mocker.patch.object(file, "exists", return_value=True)
mocker.patch.object(file, "read", return_value=config_document)
mocker.patch.object(
file, "write", return_value=lambda new: merge_dicts(config_document, new)
)
mocker.patch(
"poetry.config.config_source.ConfigSource.file",
new_callable=mocker.PropertyMock,
return_value=file,
)
@pytest.fixture
def config(config_source):
c = Config()
return Config(TomlFile(f.name)) return c
def mock_clone(_, source, dest): def mock_clone(_, source, dest):
...@@ -108,3 +134,11 @@ def http(): ...@@ -108,3 +134,11 @@ def http():
yield httpretty yield httpretty
httpretty.disable() httpretty.disable()
@pytest.fixture
def fixture_dir():
def _fixture_dir(name):
return Path(__file__).parent / "fixtures" / name
return _fixture_dir
...@@ -7,10 +7,8 @@ from poetry.utils.env import EnvManager ...@@ -7,10 +7,8 @@ from poetry.utils.env import EnvManager
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
def test_none_activated(app, tmp_dir, config): def test_none_activated(app, tmp_dir):
app.poetry._config = config app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name( venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent) "simple_project", str(app.poetry.file.parent)
...@@ -32,10 +30,8 @@ def test_none_activated(app, tmp_dir, config): ...@@ -32,10 +30,8 @@ def test_none_activated(app, tmp_dir, config):
assert expected == tester.io.fetch_output() assert expected == tester.io.fetch_output()
def test_activated(app, tmp_dir, config): def test_activated(app, tmp_dir):
app.poetry._config = config app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name( venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent) "simple_project", str(app.poetry.file.parent)
......
...@@ -7,10 +7,8 @@ from .test_use import Version ...@@ -7,10 +7,8 @@ from .test_use import Version
from .test_use import check_output_wrapper from .test_use import check_output_wrapper
def test_remove_by_python_version(app, tmp_dir, config, mocker): def test_remove_by_python_version(app, tmp_dir, mocker):
app.poetry._config = config app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name( venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent) "simple_project", str(app.poetry.file.parent)
...@@ -37,10 +35,8 @@ def test_remove_by_python_version(app, tmp_dir, config, mocker): ...@@ -37,10 +35,8 @@ def test_remove_by_python_version(app, tmp_dir, config, mocker):
assert expected == tester.io.fetch_output() assert expected == tester.io.fetch_output()
def test_remove_by_name(app, tmp_dir, config, mocker): def test_remove_by_name(app, tmp_dir):
app.poetry._config = config app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name( venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent) "simple_project", str(app.poetry.file.parent)
......
...@@ -36,15 +36,11 @@ def check_output_wrapper(version=Version.parse("3.7.1")): ...@@ -36,15 +36,11 @@ def check_output_wrapper(version=Version.parse("3.7.1")):
return check_output return check_output
def test_activate_activates_non_existing_virtualenv_no_envs_file( def test_activate_activates_non_existing_virtualenv_no_envs_file(app, tmp_dir, mocker):
app, tmp_dir, config, mocker
):
app.poetry._config = config
if "VIRTUAL_ENV" in os.environ: if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"] del os.environ["VIRTUAL_ENV"]
config.add_property("settings.virtualenvs.path", str(tmp_dir)) app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils._compat.subprocess.check_output", "poetry.utils._compat.subprocess.check_output",
...@@ -87,10 +83,8 @@ Using virtualenv: {} ...@@ -87,10 +83,8 @@ Using virtualenv: {}
def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
app, tmp_dir, config, mocker app, tmp_dir, mocker
): ):
app.poetry._config = config
os.environ["VIRTUAL_ENV"] = "/environment/prefix" os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name( venv_name = EnvManager.generate_env_name(
...@@ -100,7 +94,7 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( ...@@ -100,7 +94,7 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
python_minor = ".".join(str(v) for v in current_python[:2]) python_minor = ".".join(str(v) for v in current_python[:2])
python_patch = ".".join(str(v) for v in current_python) python_patch = ".".join(str(v) for v in current_python)
config.add_property("settings.virtualenvs.path", str(tmp_dir)) app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
(Path(tmp_dir) / "{}-py{}".format(venv_name, python_minor)).mkdir() (Path(tmp_dir) / "{}-py{}".format(venv_name, python_minor)).mkdir()
envs_file = TomlFile(Path(tmp_dir) / "envs.toml") envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
...@@ -131,10 +125,8 @@ Using virtualenv: {} ...@@ -131,10 +125,8 @@ Using virtualenv: {}
def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var( def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var(
app, tmp_dir, config, mocker app, tmp_dir, mocker
): ):
app.poetry._config = config
os.environ["VIRTUAL_ENV"] = "/environment/prefix" os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name( venv_name = EnvManager.generate_env_name(
...@@ -143,7 +135,7 @@ def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var( ...@@ -143,7 +135,7 @@ def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var(
current_python = sys.version_info[:3] current_python = sys.version_info[:3]
python_minor = ".".join(str(v) for v in current_python[:2]) python_minor = ".".join(str(v) for v in current_python[:2])
config.add_property("settings.virtualenvs.path", str(tmp_dir)) app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils.env.EnvManager._env", "poetry.utils.env.EnvManager._env",
......
import pytest import json
import tempfile import os
from cleo.testers import CommandTester from cleo.testers import CommandTester
from poetry.config import Config from poetry.config.config_source import ConfigSource
from poetry.utils.toml_file import TomlFile from poetry.poetry import Poetry
@pytest.fixture def test_list_displays_default_value_if_not_set(app, config_source):
def config(): command = app.find("config")
with tempfile.NamedTemporaryFile() as f: tester = CommandTester(command)
f.close()
tester.execute("--list")
expected = """cache-dir = "/foo"
virtualenvs.create = true
virtualenvs.in-project = false
virtualenvs.path = {path} # /foo{sep}virtualenvs
""".format(
path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep
)
return Config(TomlFile(f.name)) assert expected == tester.io.fetch_output()
@pytest.fixture(autouse=True) def test_list_displays_set_get_setting(app, config_source, config_document):
def setup(config): command = app.find("config")
config.add_property("settings.virtualenvs.path", ".") tester = CommandTester(command)
tester.execute("virtualenvs.create false")
tester.execute("--list")
yield expected = """cache-dir = "/foo"
virtualenvs.create = false
virtualenvs.in-project = false
virtualenvs.path = {path} # /foo{sep}virtualenvs
""".format(
path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep
)
config.remove_property("settings.virtualenvs.path") assert expected == tester.io.fetch_output()
def test_list_displays_default_value_if_not_set(app, config): def test_display_single_setting(app, config_source):
command = app.find("config") command = app.find("config")
command._settings_config = Config(config.file)
tester = CommandTester(command) tester = CommandTester(command)
tester.execute("--list") tester.execute("virtualenvs.create")
expected = """settings.virtualenvs.create = true expected = """true
settings.virtualenvs.in-project = false
settings.virtualenvs.path = "."
repositories = {}
""" """
assert expected == tester.io.fetch_output() assert expected == tester.io.fetch_output()
def test_list_displays_set_get_setting(app, config): def test_display_single_local_setting(app, config_source, fixture_dir):
poetry = Poetry.create(fixture_dir("with_local_config"))
app._poetry = poetry
command = app.find("config") command = app.find("config")
command._settings_config = Config(config.file)
tester = CommandTester(command) tester = CommandTester(command)
tester.execute("settings.virtualenvs.create false") tester.execute("virtualenvs.create")
command._settings_config = Config(config.file)
tester.execute("--list")
expected = """settings.virtualenvs.create = false expected = """false
settings.virtualenvs.in-project = false
settings.virtualenvs.path = "."
repositories = {}
""" """
assert expected == tester.io.fetch_output() assert expected == tester.io.fetch_output()
def test_display_single_setting(app, config): def test_list_displays_set_get_local_setting(
app, config_source, config_document, mocker
):
init = mocker.spy(ConfigSource, "__init__")
command = app.find("config") command = app.find("config")
command._settings_config = Config(config.file)
tester = CommandTester(command) tester = CommandTester(command)
tester.execute("settings.virtualenvs.create") tester.execute("virtualenvs.create false --local")
expected = """true tester.execute("--list")
"""
expected = """cache-dir = "/foo"
virtualenvs.create = false
virtualenvs.in-project = false
virtualenvs.path = {path} # /foo{sep}virtualenvs
""".format(
path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep
)
assert expected == tester.io.fetch_output() assert expected == tester.io.fetch_output()
assert "poetry.toml" == init.call_args_list[2][0][1].path.name
...@@ -8,9 +8,7 @@ except ImportError: ...@@ -8,9 +8,7 @@ except ImportError:
import urlparse import urlparse
from cleo import ApplicationTester from cleo import ApplicationTester
from tomlkit import document
from poetry.config import Config as BaseConfig
from poetry.console import Application as BaseApplication from poetry.console import Application as BaseApplication
from poetry.installation.noop_installer import NoopInstaller from poetry.installation.noop_installer import NoopInstaller
from poetry.poetry import Poetry as BasePoetry from poetry.poetry import Poetry as BasePoetry
...@@ -110,11 +108,6 @@ class Application(BaseApplication): ...@@ -110,11 +108,6 @@ class Application(BaseApplication):
self._poetry._pool = poetry.pool self._poetry._pool = poetry.pool
class Config(BaseConfig):
def __init__(self, _):
self._content = document()
class Locker(BaseLocker): class Locker(BaseLocker):
def __init__(self, lock, local_config): def __init__(self, lock, local_config):
self._lock = TomlFile(lock) self._lock = TomlFile(lock)
...@@ -154,13 +147,12 @@ class Locker(BaseLocker): ...@@ -154,13 +147,12 @@ class Locker(BaseLocker):
class Poetry(BasePoetry): class Poetry(BasePoetry):
def __init__(self, file, local_config, package, locker): def __init__(self, file, local_config, package, locker, config):
self._file = TomlFile(file) self._file = TomlFile(file)
self._package = package self._package = package
self._local_config = local_config self._local_config = local_config
self._locker = Locker(locker.lock.path, locker._local_config) self._locker = Locker(locker.lock.path, locker._local_config)
self._config = Config.create("config.toml") self._config = config
self._auth_config = Config.create("auth.toml")
# Configure sources # Configure sources
self._pool = Pool() self._pool = Pool()
...@@ -189,7 +181,7 @@ def project_directory(): ...@@ -189,7 +181,7 @@ def project_directory():
@pytest.fixture @pytest.fixture
def poetry(repo, project_directory): def poetry(repo, project_directory, config_source):
p = Poetry.create(Path(__file__).parent.parent / "fixtures" / project_directory) p = Poetry.create(Path(__file__).parent.parent / "fixtures" / project_directory)
with p.file.path.open(encoding="utf-8") as f: with p.file.path.open(encoding="utf-8") as f:
......
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
keywords = ["packaging", "dependency", "poetry"]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
requests = { version = "^2.18", optional = true, extras=[ "security" ] }
pathlib2 = { version = "^2.2", python = "~2.7" }
orator = { version = "^0.9", optional = true }
# File dependency
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }
# Dir dependency with setup.py
my-package = { path = "../project_with_setup/" }
# Dir dependency with pyproject.toml
simple-project = { path = "../simple_project/" }
[tool.poetry.extras]
db = [ "orator" ]
[tool.poetry.dev-dependencies]
pytest = "~3.4"
[tool.poetry.scripts]
my-script = "my_package:main"
[tool.poetry.plugins."blogtool.parsers"]
".rst" = "some_module::SomeClass"
import pytest
from poetry.io.null_io import NullIO
from poetry.masonry.publishing.publisher import Publisher
from poetry.poetry import Poetry
def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker):
uploader_auth = mocker.patch("poetry.masonry.publishing.uploader.Uploader.auth")
uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload")
poetry = Poetry.create(fixture_dir("sample_project"))
poetry.config.merge(
{"http-basic": {"pypi": {"username": "foo", "password": "bar"}}}
)
publisher = Publisher(poetry, NullIO())
publisher.publish(None, None, None)
assert [("foo", "bar")] == uploader_auth.call_args
assert [("https://upload.pypi.org/legacy/",)] == uploader_upload.call_args
def test_publish_can_publish_to_given_repository(fixture_dir, mocker):
uploader_auth = mocker.patch("poetry.masonry.publishing.uploader.Uploader.auth")
uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload")
poetry = Poetry.create(fixture_dir("sample_project"))
poetry.config.merge(
{
"repositories": {"my-repo": {"url": "http://foo.bar"}},
"http-basic": {"my-repo": {"username": "foo", "password": "bar"}},
}
)
publisher = Publisher(poetry, NullIO())
publisher.publish("my-repo", None, None)
assert [("foo", "bar")] == uploader_auth.call_args
assert [("http://foo.bar",)] == uploader_upload.call_args
def test_publish_raises_error_for_undefined_repository(fixture_dir, mocker):
poetry = Poetry.create(fixture_dir("sample_project"))
poetry.config.merge(
{"http-basic": {"my-repo": {"username": "foo", "password": "bar"}}}
)
publisher = Publisher(poetry, NullIO())
with pytest.raises(RuntimeError):
publisher.publish("my-repo", None, None)
import os
import pytest
import sys
@pytest.mark.skipif(
sys.platform == "win32", reason="Permissions are different on Windows"
)
def test_config_sets_the_proper_file_permissions(config):
config.add_property("settings.virtualenvs.create", True)
mode = oct(os.stat(str(config.file)).st_mode & 0o777)
assert int(mode, 8) == 384
def test_config_add_property(config):
config.add_property("settings.virtualenvs.create", True)
content = config.file.read()
assert content == {"settings": {"virtualenvs": {"create": True}}}
config.remove_property("settings.virtualenvs.create")
content = config.file.read()
assert content == {"settings": {"virtualenvs": {}}}
...@@ -196,3 +196,10 @@ The Poetry configuration is invalid: ...@@ -196,3 +196,10 @@ The Poetry configuration is invalid:
- 'description' is a required property - 'description' is a required property
""" """
assert expected == str(e.value) assert expected == str(e.value)
def test_poetry_with_local_config(fixture_dir):
poetry = Poetry.create(fixture_dir("with_local_config"))
assert not poetry.config.get("virtualenvs.in-project")
assert not poetry.config.get("virtualenvs.create")
...@@ -77,7 +77,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( ...@@ -77,7 +77,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
if "VIRTUAL_ENV" in os.environ: if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"] del os.environ["VIRTUAL_ENV"]
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils._compat.subprocess.check_output", "poetry.utils._compat.subprocess.check_output",
...@@ -114,7 +114,7 @@ def test_activate_activates_existing_virtualenv_no_envs_file(tmp_dir, config, mo ...@@ -114,7 +114,7 @@ def test_activate_activates_existing_virtualenv_no_envs_file(tmp_dir, config, mo
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name))) os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils._compat.subprocess.check_output", "poetry.utils._compat.subprocess.check_output",
...@@ -153,7 +153,7 @@ def test_activate_activates_same_virtualenv_with_envs_file(tmp_dir, config, mock ...@@ -153,7 +153,7 @@ def test_activate_activates_same_virtualenv_with_envs_file(tmp_dir, config, mock
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name))) os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils._compat.subprocess.check_output", "poetry.utils._compat.subprocess.check_output",
...@@ -192,7 +192,7 @@ def test_activate_activates_different_virtualenv_with_envs_file( ...@@ -192,7 +192,7 @@ def test_activate_activates_different_virtualenv_with_envs_file(
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name))) os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils._compat.subprocess.check_output", "poetry.utils._compat.subprocess.check_output",
...@@ -231,7 +231,7 @@ def test_activate_activates_recreates_for_different_patch(tmp_dir, config, mocke ...@@ -231,7 +231,7 @@ def test_activate_activates_recreates_for_different_patch(tmp_dir, config, mocke
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name))) os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils._compat.subprocess.check_output", "poetry.utils._compat.subprocess.check_output",
...@@ -286,7 +286,7 @@ def test_activate_does_not_recreate_when_switching_minor(tmp_dir, config, mocker ...@@ -286,7 +286,7 @@ def test_activate_does_not_recreate_when_switching_minor(tmp_dir, config, mocker
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name))) os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
os.mkdir(os.path.join(tmp_dir, "{}-py3.6".format(venv_name))) os.mkdir(os.path.join(tmp_dir, "{}-py3.6".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils._compat.subprocess.check_output", "poetry.utils._compat.subprocess.check_output",
...@@ -329,7 +329,7 @@ def test_deactivate_non_activated_but_existing(tmp_dir, config, mocker): ...@@ -329,7 +329,7 @@ def test_deactivate_non_activated_but_existing(tmp_dir, config, mocker):
/ "{}-py{}".format(venv_name, ".".join(str(c) for c in sys.version_info[:2])) / "{}-py{}".format(venv_name, ".".join(str(c) for c in sys.version_info[:2]))
).mkdir() ).mkdir()
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils._compat.subprocess.check_output", "poetry.utils._compat.subprocess.check_output",
...@@ -368,7 +368,7 @@ def test_deactivate_activated(tmp_dir, config, mocker): ...@@ -368,7 +368,7 @@ def test_deactivate_activated(tmp_dir, config, mocker):
} }
envs_file.write(doc) envs_file.write(doc)
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
mocker.patch( mocker.patch(
"poetry.utils._compat.subprocess.check_output", "poetry.utils._compat.subprocess.check_output",
...@@ -394,7 +394,7 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( ...@@ -394,7 +394,7 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir() (Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
envs_file = TomlFile(Path(tmp_dir) / "envs.toml") envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
...@@ -418,7 +418,7 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( ...@@ -418,7 +418,7 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
def test_list(tmp_dir, config): def test_list(tmp_dir, config):
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir() (Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
...@@ -432,7 +432,7 @@ def test_list(tmp_dir, config): ...@@ -432,7 +432,7 @@ def test_list(tmp_dir, config):
def test_remove_by_python_version(tmp_dir, config, mocker): def test_remove_by_python_version(tmp_dir, config, mocker):
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir() (Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
...@@ -452,7 +452,7 @@ def test_remove_by_python_version(tmp_dir, config, mocker): ...@@ -452,7 +452,7 @@ def test_remove_by_python_version(tmp_dir, config, mocker):
def test_remove_by_name(tmp_dir, config, mocker): def test_remove_by_name(tmp_dir, config, mocker):
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir() (Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
...@@ -472,7 +472,7 @@ def test_remove_by_name(tmp_dir, config, mocker): ...@@ -472,7 +472,7 @@ def test_remove_by_name(tmp_dir, config, mocker):
def test_remove_also_deactivates(tmp_dir, config, mocker): def test_remove_also_deactivates(tmp_dir, config, mocker):
config.add_property("settings.virtualenvs.path", str(tmp_dir)) config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name("simple_project", str(CWD)) venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir() (Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
......
...@@ -52,14 +52,13 @@ zipfile36>=0.1.0.0,<0.2.0.0 ...@@ -52,14 +52,13 @@ zipfile36>=0.1.0.0,<0.2.0.0
def test_get_http_basic_auth(config): def test_get_http_basic_auth(config):
config.add_property("http-basic.foo.username", "foo") config.merge({"http-basic": {"foo": {"username": "foo", "password": "bar"}}})
config.add_property("http-basic.foo.password", "bar")
assert get_http_basic_auth(config, "foo") == ("foo", "bar") assert get_http_basic_auth(config, "foo") == ("foo", "bar")
def test_get_http_basic_auth_without_password(config): def test_get_http_basic_auth_without_password(config):
config.add_property("http-basic.foo.username", "foo") config.merge({"http-basic": {"foo": {"username": "foo"}}})
assert get_http_basic_auth(config, "foo") == ("foo", None) assert get_http_basic_auth(config, "foo") == ("foo", None)
......
...@@ -109,13 +109,13 @@ def test_keyring_repository_password_del( ...@@ -109,13 +109,13 @@ def test_keyring_repository_password_del(
keyring, config, repository, username, password keyring, config, repository, username, password
): ):
keyring.set_password(keyring_service_name(repository), username, password) keyring.set_password(keyring_service_name(repository), username, password)
config.add_property("http-basic.{}.username".format(repository), username) config.merge({"http-basic": {repository: {"username": username}}})
keyring_repository_password_del(config, repository) keyring_repository_password_del(config, repository)
assert keyring.get_password(keyring_service_name(repository), username) is None assert keyring.get_password(keyring_service_name(repository), username) is None
def test_keyring_repository_password_del_not_set(keyring, config, repository, username): def test_keyring_repository_password_del_not_set(keyring, config, repository, username):
config.add_property("http-basic.{}.username".format(repository), username) config.merge({"http-basic": {repository: {"username": username}}})
keyring_repository_password_del(config, repository) keyring_repository_password_del(config, repository)
assert keyring.get_password(keyring_service_name(repository), username) is None assert keyring.get_password(keyring_service_name(repository), username) is None
......
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