Repository: sarugaku/requirementslib

Issue:
Build locations are deleted for local source directories before they can be referenced
When building a local source package via `SetupInfo` on master, the build directory created is temporary and is deleted by pip before it can be referenced and inspected for dependencies

Discussion:

Patch:
diff --git a/news/200.bugfix.rst b/news/200.bugfix.rst
new file mode 100644
index 0000000..4a5caa7
--- /dev/null
+++ b/news/200.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed an issue which caused build directories to be deleted before dependencies could be determined for editable source reqiurements.
diff --git a/news/202.bugfix.rst b/news/202.bugfix.rst
new file mode 100644
index 0000000..3cc180d
--- /dev/null
+++ b/news/202.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed a bug which could cause parsing to fail for ``setup.cfg`` files on python 2.
diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py
index eb12b04..1ec47f7 100644
--- a/src/requirementslib/models/requirements.py
+++ b/src/requirementslib/models/requirements.py
@@ -83,6 +83,7 @@ from .utils import (
     make_install_requirement,
     normalize_name,
     parse_extras,
+    read_source,
     specs_to_string,
     split_markers_from_line,
     split_ref_from_uri,
@@ -862,10 +863,16 @@ class Line(object):
     @cached_property
     def parsed_setup_cfg(self):
         # type: () -> Dict[Any, Any]
-        if self.is_local and self.path and is_installable_dir(self.path):
-            if self.setup_cfg:
-                return parse_setup_cfg(self.setup_cfg)
-        return {}
+        if not (
+            self.is_local
+            and self.path
+            and is_installable_dir(self.path)
+            and self.setup_cfg
+        ):
+            return {}
+        base_dir = os.path.dirname(os.path.abspath(self.setup_cfg))
+        setup_content = read_source(self.setup_cfg)
+        return parse_setup_cfg(setup_content, base_dir)
 
     @cached_property
     def parsed_setup_py(self):
diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py
index 38cc838..8ad7373 100644
--- a/src/requirementslib/models/setup_info.py
+++ b/src/requirementslib/models/setup_info.py
@@ -5,6 +5,7 @@ import ast
 import atexit
 import contextlib
 import importlib
+import io
 import os
 import shutil
 import sys
@@ -280,14 +281,8 @@ def get_extras_from_setupcfg(parser):
     return extras
 
 
-def parse_setup_cfg(setup_cfg_path):
-    # type: (S) -> Dict[S, Union[S, None, Set[BaseRequirement], List[S], Dict[STRING_TYPE, Tuple[BaseRequirement]]]]
-    if not os.path.exists(setup_cfg_path):
-        raise FileNotFoundError(setup_cfg_path)
-    try:
-        return setuptools_parse_setup_cfg(setup_cfg_path)
-    except Exception:
-        pass
+def parse_setup_cfg(setup_cfg_contents, base_dir):
+    # type: (S, S) -> Dict[S, Union[S, None, Set[BaseRequirement], List[S], Dict[STRING_TYPE, Tuple[BaseRequirement]]]]
     default_opts = {
         "metadata": {"name": "", "version": ""},
         "options": {
@@ -300,9 +295,12 @@ def parse_setup_cfg(setup_cfg_path):
         },
     }
     parser = configparser.ConfigParser(default_opts)
-    parser.read(setup_cfg_path)
+    if six.PY2:
+        buff = io.BytesIO(setup_cfg_contents)
+        parser.readfp(buff)
+    else:
+        parser.read_string(setup_cfg_contents)
     results = {}
-    base_dir = os.path.dirname(os.path.abspath(setup_cfg_path))
     package_dir = get_package_dir_from_setupcfg(parser, base_dir=base_dir)
     name, version = get_name_and_version_from_setupcfg(parser, package_dir)
     results["name"] = name
@@ -1031,11 +1029,6 @@ class SetupInfo(object):
             self._version = info.get("version", None)
         return self._version
 
-    @classmethod
-    def get_setup_cfg(cls, setup_cfg_path):
-        # type: (S) -> Dict[S, Union[S, None, Set[BaseRequirement], List[S], Tuple[S, Tuple[BaseRequirement]]]]
-        return parse_setup_cfg(setup_cfg_path)
-
     @property
     def egg_base(self):
         # type: () -> S
@@ -1118,7 +1111,14 @@ class SetupInfo(object):
     def parse_setup_cfg(self):
         # type: () -> Dict[STRING_TYPE, Any]
         if self.setup_cfg is not None and self.setup_cfg.exists():
-            parsed = self.get_setup_cfg(self.setup_cfg.as_posix())
+            contents = self.setup_cfg.read_text()
+            base_dir = self.setup_cfg.absolute().parent.as_posix()
+            try:
+                parsed = setuptools_parse_setup_cfg(self.setup_cfg.as_posix())
+            except Exception:
+                if six.PY2:
+                    contents = self.setup_cfg.read_bytes()
+                parsed = parse_setup_cfg(contents, base_dir)
             if not parsed:
                 return {}
             return parsed
@@ -1481,6 +1481,12 @@ build-backend = "{1}"
             raise RequirementError(
                 "The file URL points to a directory not installable: {}".format(ireq.link)
             )
+        # this ensures the build dir is treated as the temporary build location
+        # and the source dir is treated as permanent / not deleted by pip
+        build_location_func = getattr(ireq, "build_location", None)
+        if build_location_func is None:
+            build_location_func = getattr(ireq, "ensure_build_location", None)
+        build_location_func(kwargs["build_dir"])
         ireq.ensure_has_source_dir(kwargs["src_dir"])
         src_dir = ireq.source_dir
 


Test patch:
diff --git a/tests/unit/test_setup_info.py b/tests/unit/test_setup_info.py
index c04fb2a..1878d77 100644
--- a/tests/unit/test_setup_info.py
+++ b/tests/unit/test_setup_info.py
@@ -212,8 +212,11 @@ def test_ast_parser_finds_fully_qualified_setup(setup_py_dir):
 
 
 def test_setup_cfg_parser(setup_cfg_dir):
-    result = parse_setup_cfg(
-        (setup_cfg_dir / "package_with_multiple_extras/setup.cfg").as_posix()
-    )
+    setup_path = setup_cfg_dir / "package_with_multiple_extras/setup.cfg"
+    if six.PY2:
+        contents = setup_path.read_bytes()
+    else:
+        contents = setup_path.read_text()
+    result = parse_setup_cfg(contents, setup_path.parent.as_posix())
     assert result["version"] == "0.5.0"
     assert result["name"] == "test_package"