diff --git a/poetry/factory.py b/poetry/factory.py index 472a6ec7ed1..531cca75adb 100755 --- a/poetry/factory.py +++ b/poetry/factory.py @@ -49,6 +49,16 @@ def create_poetry( config.merge(local_config_file.read()) + # Load local sources + repositories = {} + for source in base_poetry.pyproject.poetry_config.get("source", []): + name = source.get("name") + url = source.get("url") + if name and url: + repositories[name] = {"url": url} + + config.merge({"repositories": repositories}) + poetry = Poetry( base_poetry.file.path, base_poetry.local_config, @@ -124,11 +134,9 @@ def create_config(cls, io=None): # type: (Optional[IO]) -> Config def create_legacy_repository( self, source, auth_config ): # type: (Dict[str, str], Config) -> LegacyRepository - from .repositories.auth import Auth from .repositories.legacy_repository import LegacyRepository from .utils.helpers import get_cert from .utils.helpers import get_client_cert - from .utils.password_manager import PasswordManager if "url" in source: # PyPI-like repository @@ -137,19 +145,13 @@ def create_legacy_repository( else: raise RuntimeError("Unsupported source specified") - password_manager = PasswordManager(auth_config) name = source["name"] url = source["url"] - credentials = password_manager.get_http_auth(name) - if credentials: - auth = Auth(url, credentials["username"], credentials["password"]) - else: - auth = None return LegacyRepository( name, url, - auth=auth, + config=auth_config, cert=get_cert(auth_config, name), client_cert=get_client_cert(auth_config, name), ) diff --git a/poetry/installation/authenticator.py b/poetry/installation/authenticator.py index 9d5b783ace4..69adb844809 100644 --- a/poetry/installation/authenticator.py +++ b/poetry/installation/authenticator.py @@ -1,3 +1,4 @@ +import logging import time from typing import TYPE_CHECKING @@ -21,14 +22,27 @@ from poetry.config.config import Config +logger = logging.getLogger() + + class Authenticator(object): - def __init__(self, config, io): # type: (Config, IO) -> None + def __init__(self, config, io=None): # type: (Config, Optional[IO]) -> None self._config = config self._io = io self._session = None self._credentials = {} self._password_manager = PasswordManager(self._config) + def _log(self, message, level="debug"): # type: (str, str) -> None + if self._io is not None: + self._io.write_line( + "<{level:s}>{message:s}".format( + message=message, level=level + ) + ) + else: + getattr(logger, level, logger.debug)(message) + @property def session(self): # type: () -> requests.Session if self._session is None: @@ -40,9 +54,7 @@ def request( self, method, url, **kwargs ): # type: (str, str, Any) -> requests.Response request = requests.Request(method, url) - io = kwargs.get("io") or self._io - - username, password = self._get_credentials_for_url(url) + username, password = self.get_credentials_for_url(url) if username is not None and password is not None: request = requests.auth.HTTPBasicAuth(username, password)(request) @@ -83,19 +95,16 @@ def request( if not is_last_attempt: attempt += 1 delay = 0.5 * attempt - if io is not None: - io.write_line( - "Retrying HTTP request in {} seconds.".format( - delay - ) - ) + self._log( + "Retrying HTTP request in {} seconds.".format(delay), level="debug" + ) time.sleep(delay) continue # this should never really be hit under any sane circumstance raise PoetryException("Failed HTTP {} request", method.upper()) - def _get_credentials_for_url( + def get_credentials_for_url( self, url ): # type: (str) -> Tuple[Optional[str], Optional[str]] parsed_url = urlparse.urlsplit(url) @@ -135,7 +144,8 @@ def _get_credentials_for_netloc_from_config( self, netloc ): # type: (str) -> Tuple[Optional[str], Optional[str]] credentials = (None, None) - for repository_name in self._config.get("http-basic", {}): + + for repository_name in self._config.get("repositories", []): repository_config = self._config.get( "repositories.{}".format(repository_name) ) diff --git a/poetry/repositories/auth.py b/poetry/repositories/auth.py deleted file mode 100644 index b01d51af72c..00000000000 --- a/poetry/repositories/auth.py +++ /dev/null @@ -1,27 +0,0 @@ -from requests import Request -from requests.auth import AuthBase -from requests.auth import HTTPBasicAuth - -from poetry.utils._compat import urlparse - - -class Auth(AuthBase): - def __init__(self, url, username, password): # type: (str, str, str) -> None - self._hostname = urlparse.urlparse(url).hostname - self._auth = HTTPBasicAuth(username, password) - - @property - def hostname(self): # type: () -> str - return self._hostname - - @property - def auth(self): # type: () -> HTTPBasicAuth - return self._auth - - def __call__(self, r): # type: (Request) -> Request - if urlparse.urlparse(r.url).hostname != self._hostname: - return r - - self._auth(r) - - return r diff --git a/poetry/repositories/legacy_repository.py b/poetry/repositories/legacy_repository.py index 41b678dca16..f9963ddc4d4 100755 --- a/poetry/repositories/legacy_repository.py +++ b/poetry/repositories/legacy_repository.py @@ -8,6 +8,7 @@ from typing import Union import requests +import requests.auth from cachecontrol import CacheControl from cachecontrol.caches.file_cache import FileCache @@ -24,8 +25,9 @@ from poetry.utils.helpers import canonicalize_name from poetry.utils.patterns import wheel_file_re +from ..config.config import Config from ..inspection.info import PackageInfo -from .auth import Auth +from ..installation.authenticator import Authenticator from .exceptions import PackageNotFound from .exceptions import RepositoryError from .pypi_repository import PyPiRepository @@ -159,15 +161,14 @@ def clean_link(self, url): class LegacyRepository(PyPiRepository): def __init__( - self, name, url, auth=None, disable_cache=False, cert=None, client_cert=None - ): # type: (str, str, Optional[Auth], bool, Optional[Path], Optional[Path]) -> None + self, name, url, config=None, disable_cache=False, cert=None, client_cert=None + ): # type: (str, str, Optional[Config], bool, Optional[Path], Optional[Path]) -> None if name == "pypi": raise ValueError("The name [pypi] is reserved for repositories") self._packages = [] self._name = name self._url = url.rstrip("/") - self._auth = auth self._client_cert = client_cert self._cert = cert self._cache_dir = REPOSITORY_CACHE_DIR / name @@ -183,19 +184,25 @@ def __init__( } ) + self._authenticator = Authenticator( + config=config or Config(use_environment=True) + ) + self._session = CacheControl( - requests.session(), cache=FileCache(str(self._cache_dir / "_http")) + self._authenticator.session, cache=FileCache(str(self._cache_dir / "_http")) ) - url_parts = urlparse.urlparse(self._url) - if not url_parts.username and self._auth: - self._session.auth = self._auth + username, password = self._authenticator.get_credentials_for_url(self._url) + if username is not None and password is not None: + self._authenticator.session.auth = requests.auth.HTTPBasicAuth( + username, password + ) if self._cert: - self._session.verify = str(self._cert) + self._authenticator.session.verify = str(self._cert) if self._client_cert: - self._session.cert = str(self._client_cert) + self._authenticator.session.cert = str(self._client_cert) self._disable_cache = disable_cache @@ -209,15 +216,15 @@ def client_cert(self): # type: () -> Optional[Path] @property def authenticated_url(self): # type: () -> str - if not self._auth: + if not self._session.auth: return self.url parsed = urlparse.urlparse(self.url) return "{scheme}://{username}:{password}@{netloc}{path}".format( scheme=parsed.scheme, - username=quote(self._auth.auth.username, safe=""), - password=quote(self._auth.auth.password, safe=""), + username=quote(self._session.auth.username, safe=""), + password=quote(self._session.auth.password, safe=""), netloc=parsed.netloc, path=parsed.path, ) diff --git a/tests/installation/test_authenticator.py b/tests/installation/test_authenticator.py index 4cf323ef708..d19364741d1 100644 --- a/tests/installation/test_authenticator.py +++ b/tests/installation/test_authenticator.py @@ -180,3 +180,22 @@ def callback(request, uri, response_headers): assert excinfo.value.response.text == content assert sleep.call_count == attempts + + +@pytest.fixture +def environment_repository_credentials(monkeypatch): + monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_USERNAME", "bar") + monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_PASSWORD", "baz") + + +def test_authenticator_uses_env_provided_credentials( + config, environ, mock_remote, http, environment_repository_credentials +): + config.merge({"repositories": {"foo": {"url": "https://foo.bar/simple/"}}}) + + authenticator = Authenticator(config, NullIO()) + authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") + + request = http.last_request() + + assert "Basic YmFyOmJheg==" == request.headers["Authorization"] diff --git a/tests/repositories/test_auth.py b/tests/repositories/test_auth.py deleted file mode 100644 index 2e5ab6cd752..00000000000 --- a/tests/repositories/test_auth.py +++ /dev/null @@ -1,46 +0,0 @@ -import base64 - -from requests import Request - -from poetry.repositories.auth import Auth -from poetry.utils._compat import decode -from poetry.utils._compat import encode - - -def test_auth_with_request_on_the_same_host(): - auth = Auth("https://python-poetry.org", "foo", "bar") - - request = Request("GET", "https://python-poetry.org/docs/") - assert "Authorization" not in request.headers - - request = auth(request) - - assert "Authorization" in request.headers - assert request.headers["Authorization"] == "Basic {}".format( - decode(base64.b64encode(encode(":".join(("foo", "bar"))))) - ) - - -def test_auth_with_request_with_same_authentication(): - auth = Auth("https://python-poetry.org", "foo", "bar") - - request = Request("GET", "https://foo:bar@python-poetry.org/docs/") - assert "Authorization" not in request.headers - - request = auth(request) - - assert "Authorization" in request.headers - assert request.headers["Authorization"] == "Basic {}".format( - decode(base64.b64encode(encode(":".join(("foo", "bar"))))) - ) - - -def test_auth_with_request_on_different_hosts(): - auth = Auth("https://python-poetry.org", "foo", "bar") - - request = Request("GET", "https://pendulum.eustace.io/docs/") - assert "Authorization" not in request.headers - - request = auth(request) - - assert "Authorization" not in request.headers diff --git a/tests/repositories/test_legacy_repository.py b/tests/repositories/test_legacy_repository.py index 1031a48f6c2..7fd131c70e4 100644 --- a/tests/repositories/test_legacy_repository.py +++ b/tests/repositories/test_legacy_repository.py @@ -4,7 +4,6 @@ from poetry.core.packages import Dependency from poetry.factory import Factory -from poetry.repositories.auth import Auth from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.exceptions import RepositoryError from poetry.repositories.legacy_repository import LegacyRepository @@ -23,9 +22,9 @@ class MockRepository(LegacyRepository): FIXTURES = Path(__file__).parent / "fixtures" / "legacy" - def __init__(self, auth=None): + def __init__(self): super(MockRepository, self).__init__( - "legacy", url="http://legacy.foo.bar", auth=auth, disable_cache=True + "legacy", url="http://legacy.foo.bar", disable_cache=True ) def _get(self, endpoint): @@ -302,18 +301,11 @@ def test_get_package_retrieves_packages_with_no_hashes(): assert [] == package.files -def test_username_password_special_chars(): - auth = Auth("http://legacy.foo.bar", "user:", "/%2Fp@ssword") - repo = MockRepository(auth=auth) - - assert "http://user%3A:%2F%252Fp%40ssword@legacy.foo.bar" == repo.authenticated_url - - class MockHttpRepository(LegacyRepository): def __init__(self, endpoint_responses, http): base_url = "http://legacy.foo.bar" super(MockHttpRepository, self).__init__( - "legacy", url=base_url, auth=None, disable_cache=True + "legacy", url=base_url, disable_cache=True ) for endpoint, response in endpoint_responses.items(): diff --git a/tests/utils/test_exporter.py b/tests/utils/test_exporter.py index 99fb48a18e9..3a043dc0e9f 100644 --- a/tests/utils/test_exporter.py +++ b/tests/utils/test_exporter.py @@ -4,7 +4,6 @@ from poetry.factory import Factory from poetry.packages import Locker as BaseLocker -from poetry.repositories.auth import Auth from poetry.repositories.legacy_repository import LegacyRepository from poetry.utils._compat import Path from poetry.utils.exporter import Exporter @@ -654,11 +653,7 @@ def test_exporter_can_export_requirements_txt_with_file_packages_and_markers( def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, poetry): poetry.pool.add_repository( - LegacyRepository( - "custom", - "https://example.com/simple", - auth=Auth("https://example.com/simple", "foo", "bar"), - ) + LegacyRepository("custom", "https://example.com/simple",) ) poetry.locker.mock_lock_data( { @@ -713,11 +708,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so tmp_dir, poetry ): poetry.pool.add_repository( - LegacyRepository( - "custom", - "https://example.com/simple", - auth=Auth("https://example.com/simple", "foo", "bar"), - ) + LegacyRepository("custom", "https://example.com/simple",) ) poetry.pool.add_repository(LegacyRepository("custom", "https://foobaz.com/simple",)) poetry.locker.mock_lock_data( @@ -792,12 +783,14 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials( tmp_dir, poetry, config ): + poetry.config.merge( + { + "repositories": {"custom": {"url": "https://example.com/simple"}}, + "http-basic": {"custom": {"username": "foo", "password": "bar"}}, + } + ) poetry.pool.add_repository( - LegacyRepository( - "custom", - "https://example.com/simple", - auth=Auth("https://example.com/simple", "foo", "bar"), - ) + LegacyRepository("custom", "https://example.com/simple", config=poetry.config) ) poetry.locker.mock_lock_data( {