import urllib.parse

from pathlib import Path
from typing import Optional
from typing import Sequence
from typing import Union

from cleo.io.io import IO

from poetry.core.packages.utils.utils import path_to_url
from poetry.poetry import Poetry
from poetry.utils._compat import decode


class Exporter:
    """
    Exporter class to export a lock file to alternative formats.
    """

    FORMAT_REQUIREMENTS_TXT = "requirements.txt"
    #: The names of the supported export formats.
    ACCEPTED_FORMATS = (FORMAT_REQUIREMENTS_TXT,)
    ALLOWED_HASH_ALGORITHMS = ("sha256", "sha384", "sha512")

    def __init__(self, poetry: Poetry) -> None:
        self._poetry = poetry

    def export(
        self,
        fmt: str,
        cwd: Path,
        output: Union[IO, str],
        with_hashes: bool = True,
        dev: bool = False,
        extras: Optional[Union[bool, Sequence[str]]] = None,
        with_credentials: bool = False,
    ) -> None:
        if fmt not in self.ACCEPTED_FORMATS:
            raise ValueError(f"Invalid export format: {fmt}")

        getattr(self, "_export_{}".format(fmt.replace(".", "_")))(
            cwd,
            output,
            with_hashes=with_hashes,
            dev=dev,
            extras=extras,
            with_credentials=with_credentials,
        )

    def _export_requirements_txt(
        self,
        cwd: Path,
        output: Union[IO, str],
        with_hashes: bool = True,
        dev: bool = False,
        extras: Optional[Union[bool, Sequence[str]]] = None,
        with_credentials: bool = False,
    ) -> None:
        indexes = set()
        content = ""
        dependency_lines = set()

        for dependency_package in self._poetry.locker.get_project_dependency_packages(
            project_requires=self._poetry.package.all_requires, dev=dev, extras=extras
        ):
            line = ""

            dependency = dependency_package.dependency
            package = dependency_package.package

            if package.develop:
                line += "-e "

            requirement = dependency.to_pep_508(with_extras=False)
            is_direct_local_reference = (
                dependency.is_file() or dependency.is_directory()
            )
            is_direct_remote_reference = dependency.is_vcs() or dependency.is_url()

            if is_direct_remote_reference:
                line = requirement
            elif is_direct_local_reference:
                dependency_uri = path_to_url(dependency.source_url)
                line = f"{dependency.name} @ {dependency_uri}"
            else:
                line = f"{package.name}=={package.version}"

            if not is_direct_remote_reference:
                if ";" in requirement:
                    markers = requirement.split(";", 1)[1].strip()
                    if markers:
                        line += f"; {markers}"

            if (
                not is_direct_remote_reference
                and not is_direct_local_reference
                and package.source_url
            ):
                indexes.add(package.source_url)

            if package.files and with_hashes:
                hashes = []
                for f in package.files:
                    h = f["hash"]
                    algorithm = "sha256"
                    if ":" in h:
                        algorithm, h = h.split(":")

                        if algorithm not in self.ALLOWED_HASH_ALGORITHMS:
                            continue

                    hashes.append(f"{algorithm}:{h}")

                if hashes:
                    line += " \\\n"
                    for i, h in enumerate(hashes):
                        line += "    --hash={}{}".format(
                            h, " \\\n" if i < len(hashes) - 1 else ""
                        )
            dependency_lines.add(line)

        content += "\n".join(sorted(dependency_lines))
        content += "\n"

        if indexes:
            # If we have extra indexes, we add them to the beginning of the output
            indexes_header = ""
            for index in sorted(indexes):
                repositories = [
                    r
                    for r in self._poetry.pool.repositories
                    if r.url == index.rstrip("/")
                ]
                if not repositories:
                    continue
                repository = repositories[0]
                if (
                    self._poetry.pool.has_default()
                    and repository is self._poetry.pool.repositories[0]
                ):
                    url = (
                        repository.authenticated_url
                        if with_credentials
                        else repository.url
                    )
                    indexes_header = f"--index-url {url}\n"
                    continue

                url = (
                    repository.authenticated_url if with_credentials else repository.url
                )
                parsed_url = urllib.parse.urlsplit(url)
                if parsed_url.scheme == "http":
                    indexes_header += f"--trusted-host {parsed_url.netloc}\n"
                indexes_header += f"--extra-index-url {url}\n"

            content = indexes_header + "\n" + content

        self._output(content, cwd, output)

    def _output(self, content: str, cwd: Path, output: Union[IO, str]) -> None:
        decoded = decode(content)
        try:
            output.write(decoded)
        except AttributeError:
            filepath = cwd / output
            with filepath.open("w", encoding="utf-8") as f:
                f.write(decoded)

poetry/utils/exporter.py
import urllib.parse

from pathlib import Path
from typing import Optional
from typing import Sequence
from typing import Union

from cleo.io.io import IO

from poetry.core.packages.utils.utils import path_to_url
from poetry.poetry import Poetry
from poetry.utils._compat import decode


class Exporter:
    """
    Exporter class to export a lock file to alternative formats.
    """

    FORMAT_REQUIREMENTS_TXT = "requirements.txt"
    #: The names of the supported export formats.
    ACCEPTED_FORMATS = (FORMAT_REQUIREMENTS_TXT,)
    ALLOWED_HASH_ALGORITHMS = ("sha256", "sha384", "sha512")

    def __init__(self, poetry: Poetry) -> None:
        self._poetry = poetry

    def export(
        self,
        fmt: str,
        cwd: Path,
        output: Union[IO, str],
        with_hashes: bool = True,
        dev: bool = False,
        extras: Optional[Union[bool, Sequence[str]]] = None,
        with_credentials: bool = False,
    ) -> None:
        if fmt not in self.ACCEPTED_FORMATS:
            raise ValueError(f"Invalid export format: {fmt}")

        getattr(self, "_export_{}".format(fmt.replace(".", "_")))(
            cwd,
            output,
            with_hashes=with_hashes,
            dev=dev,
            extras=extras,
            with_credentials=with_credentials,
        )

    def _export_requirements_txt(
        self,
        cwd: Path,
        output: Union[IO, str],
        with_hashes: bool = True,
        dev: bool = False,
        extras: Optional[Union[bool, Sequence[str]]] = None,
        with_credentials: bool = False,
    ) -> None:
        indexes = set()
        content = ""
        dependency_lines = set()

        for dependency_package in self._poetry.locker.get_project_dependency_packages(
            project_requires=self._poetry.package.all_requires, dev=dev, extras=extras
        ):
            line = ""

            dependency = dependency_package.dependency
            package = dependency_package.package

            if package.develop:
                line += "-e "

            requirement = dependency.to_pep_508(with_extras=False)
            is_direct_local_reference = (
                dependency.is_file() or dependency.is_directory()
            )
            is_direct_remote_reference = dependency.is_vcs() or dependency.is_url()

            if is_direct_remote_reference:
                line = requirement
            elif is_direct_local_reference:
                dependency_uri = path_to_url(dependency.source_url)
                line = f"{dependency.name} @ {dependency_uri}"
            else:
                line = f"{package.name}=={package.version}"

            if not is_direct_remote_reference:
                if ";" in requirement:
                    markers = requirement.split(";", 1)[1].strip()
                    if markers:
                        line += f" ; {markers}"

            if (
                not is_direct_remote_reference
                and not is_direct_local_reference
                and package.source_url
            ):
                indexes.add(package.source_url)

            if package.files and with_hashes:
                hashes = []
                for f in package.files:
                    h = f["hash"]
                    algorithm = "sha256"
                    if ":" in h:
                        algorithm, h = h.split(":")

                        if algorithm not in self.ALLOWED_HASH_ALGORITHMS:
                            continue

                    hashes.append(f"{algorithm}:{h}")

                if hashes:
                    line += " \\\n"
                    for i, h in enumerate(hashes):
                        line += "    --hash={}{}".format(
                            h, " \\\n" if i < len(hashes) - 1 else ""
                        )
            dependency_lines.add(line)

        content += "\n".join(sorted(dependency_lines))
        content += "\n"

        if indexes:
            # If we have extra indexes, we add them to the beginning of the output
            indexes_header = ""
            for index in sorted(indexes):
                repositories = [
                    r
                    for r in self._poetry.pool.repositories
                    if r.url == index.rstrip("/")
                ]
                if not repositories:
                    continue
                repository = repositories[0]
                if (
                    self._poetry.pool.has_default()
                    and repository is self._poetry.pool.repositories[0]
                ):
                    url = (
                        repository.authenticated_url
                        if with_credentials
                        else repository.url
                    )
                    indexes_header = f"--index-url {url}\n"
                    continue

                url = (
                    repository.authenticated_url if with_credentials else repository.url
                )
                parsed_url = urllib.parse.urlsplit(url)
                if parsed_url.scheme == "http":
                    indexes_header += f"--trusted-host {parsed_url.netloc}\n"
                indexes_header += f"--extra-index-url {url}\n"

            content = indexes_header + "\n" + content

        self._output(content, cwd, output)

    def _output(self, content: str, cwd: Path, output: Union[IO, str]) -> None:
        decoded = decode(content)
        try:
            output.write(decoded)
        except AttributeError:
            filepath = cwd / output
            with filepath.open("w", encoding="utf-8") as f:
                f.write(decoded)

poetry/utils/exporter.py
diff --git a/poetry/utils/exporter.py b/poetry/utils/exporter.py
index 2ce9c6d19fd..2268ecb222c 100644
--- a/poetry/utils/exporter.py
+++ b/poetry/utils/exporter.py
@@ -89,7 +89,7 @@ def _export_requirements_txt(
                 if ";" in requirement:
                     markers = requirement.split(";", 1)[1].strip()
                     if markers:
-                        line += f"; {markers}"
+                        line += f" ; {markers}"
 
             if (
                 not is_direct_remote_reference
diff --git a/tests/utils/test_exporter.py b/tests/utils/test_exporter.py
index 03f878aaf3b..e8507c8cbb9 100644
--- a/tests/utils/test_exporter.py
+++ b/tests/utils/test_exporter.py
@@ -164,8 +164,8 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers
 
     expected = """\
 bar==4.5.6
-baz==7.8.9; sys_platform == "win32"
-foo==1.2.3; python_version < "3.7"
+baz==7.8.9 ; sys_platform == "win32"
+foo==1.2.3 ; python_version < "3.7"
 """
 
     assert expected == content
@@ -273,10 +273,10 @@ def test_exporter_can_export_requirements_txt_poetry(tmp_dir, poetry):
         "junit-xml": Dependency.create_from_pep_508("junit-xml==1.9"),
         "keyring": Dependency.create_from_pep_508("keyring==21.8.0"),
         "secretstorage": Dependency.create_from_pep_508(
-            "secretstorage==3.3.0; sys_platform=='linux'"
+            "secretstorage==3.3.0 ; sys_platform=='linux'"
         ),
         "cryptography": Dependency.create_from_pep_508(
-            "cryptography==3.2; sys_platform=='linux'"
+            "cryptography==3.2 ; sys_platform=='linux'"
         ),
         "six": Dependency.create_from_pep_508("six==1.15.0"),
     }
@@ -353,7 +353,7 @@ def test_exporter_can_export_requirements_txt_pyinstaller(tmp_dir, poetry):
         "pyinstaller": Dependency.create_from_pep_508("pyinstaller==4.0"),
         "altgraph": Dependency.create_from_pep_508("altgraph==0.17"),
         "macholib": Dependency.create_from_pep_508(
-            "macholib==1.8; sys_platform == 'darwin'"
+            "macholib==1.8 ; sys_platform == 'darwin'"
         ),
     }
 
@@ -423,15 +423,15 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers(
         content = f.read()
 
     expected = {
-        "a": Dependency.create_from_pep_508("a==1.2.3; python_version < '3.7'"),
+        "a": Dependency.create_from_pep_508("a==1.2.3 ; python_version < '3.7'"),
         "b": Dependency.create_from_pep_508(
-            "b==4.5.6; platform_system == 'Windows' and python_version < '3.7'"
+            "b==4.5.6 ; platform_system == 'Windows' and python_version < '3.7'"
         ),
         "c": Dependency.create_from_pep_508(
-            "c==7.8.9; sys_platform == 'win32' and python_version < '3.7'"
+            "c==7.8.9 ; sys_platform == 'win32' and python_version < '3.7'"
         ),
         "d": Dependency.create_from_pep_508(
-            "d==0.0.1; platform_system == 'Windows' and python_version < '3.7' or sys_platform == 'win32' and python_version < '3.7'"
+            "d==0.0.1 ; platform_system == 'Windows' and python_version < '3.7' or sys_platform == 'win32' and python_version < '3.7'"
         ),
     }
 
@@ -447,7 +447,7 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers(
 
 @pytest.mark.parametrize(
     "dev,lines",
-    [(False, ['a==1.2.3; python_version < "3.8"']), (True, ["a==1.2.3", "b==4.5.6"])],
+    [(False, ['a==1.2.3 ; python_version < "3.8"']), (True, ["a==1.2.3", "b==4.5.6"])],
 )
 def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_any(
     tmp_dir, poetry, dev, lines
@@ -1139,7 +1139,7 @@ def test_exporter_can_export_requirements_txt_with_directory_packages_and_marker
         content = f.read()
 
     expected = """\
-foo @ {}/tests/fixtures/sample_project; python_version < "3.7"
+foo @ {}/tests/fixtures/sample_project ; python_version < "3.7"
 """.format(
         working_directory.as_uri()
     )
@@ -1228,7 +1228,7 @@ def test_exporter_can_export_requirements_txt_with_file_packages_and_markers(
         content = f.read()
 
     expected = """\
-foo @ {}/tests/fixtures/distributions/demo-0.1.0.tar.gz; python_version < "3.7"
+foo @ {}/tests/fixtures/distributions/demo-0.1.0.tar.gz ; python_version < "3.7"
 """.format(
         working_directory.as_uri()
     )

Incorrect requirements.txt formatting in poetry export 
The `requirements.txt` format needs to put a space in front of the semicolon that specifies the package and the pyversion and platform constraints.  Right now, without the space, the semicolon will be interpreted as part of a URL.  See this issue in `packaging`:
https://github.com/pypa/packaging/issues/456
