Repository: pypa/pipenv

Issue:
Evaluate pathlib compatibility for Python < 3.5
Running Python 2.7.12 with pipenv installed in user site (pip install --user pipenv) fails because 
pathlib.Path.mkdir function doesn't have a exist_ok argument specified.

Actual error (from bottom of full output):
TypeError: mkdir() got an unexpected keyword argument 'exist_ok'

Seems like it's confied to python2 as python2 doesn't have pathlib natively in the standard library and must use a backport.

Full output:
project_dir>pipenv update --dev
Running $ pipenv lock then $ pipenv sync.
Locking [dev-packages] dependencies...
Traceback (most recent call last):
  File "c:\python27\lib\runpy.py", line 174, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "c:\python27\lib\runpy.py", line 72, in _run_code
    exec code in run_globals
  File "<user_base>\Scripts\pipenv.exe\__main__.py", line 9, in <module>
  File "<user_base>\Python\Python27\site-packages\pipenv\vendor\click\core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "<user_base>\Python27\site-packages\pipenv\vendor\click\core.py", line 697, in main
    rv = self.invoke(ctx)
  File "<user_base>\Python27\site-packages\pipenv\vendor\click\core.py", line 1066, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "<user_base>\Python27\site-packages\pipenv\vendor\click\core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "<user_base>\Python27\site-packages\pipenv\vendor\click\core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "<user_base>\Python27\site-packages\pipenv\vendor\click\decorators.py", line 17, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "<user_base>\Python27\site-packages\pipenv\cli.py", line 850, in update
    verbose=verbose, clear=clear, pre=pre, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror
  File "<user_base>\Python27\site-packages\pipenv\core.py", line 1098, in do_lock
    dev=settings['dev']
  File "<user_base>\Python27\site-packages\pipenv\utils.py", line 1177, in get_vcs_deps
    src_dir.mkdir(mode=0o775, exist_ok=True)
TypeError: mkdir() got an unexpected keyword argument 'exist_ok'


[pipenv_help.txt](https://github.com/pypa/pipenv/files/2139816/pipenv_help.txt)


##### Expected result
Expected the update to run without error

##### Actual result
Failed as shown at the beginning


##### Steps to replicate

In pristine project directory (no Pipfile) using python 2.7.12 or greater run:
pipenv install future

You should actually see the problem there as well.

If not, then run "pipenv update"



Discussion:
I poked around a bit more.
I was able to reproduce this bug on both windows and linux pipenv installations.
I also examined the pathlib backport on my system

Python 2.7.12 (v2.7.12:d33e0cf91556, Jun 27 2016, 15:24:40) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import pathlib
Help on method mkdir in module pathlib:
mkdir(self, mode=511, parents=False) method of pathlib.WindowsPath instance
>>>

Possible fix is to use pathlib2 instead of pathlib, looks like it supports the exist_ok argument.

Python 2.7.12 (v2.7.12:d33e0cf91556, Jun 27 2016, 15:24:40) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import pathlib2
Help on method mkdir in module pathlib2:
mkdir(self, mode=511, parents=False, exist_ok=False) method of pathlib2.WindowsPath instance
    Create a new directory at this given path.
>>>
We fall back to pathlib2 which we vendor. I guess you have manually installed the old, deprecated `pathlib` in your global environment. I guess we can use python conditional version checks but the real solution is to stop installing pathlib in your environment. 
We do list 3.4 as supported, and the built-in pathlib on 3.4 does not have `exist_ok` IIRC. I need to fix that.
Or maybe we should use the vendored pathlib2 on 3.4.
@techalchemy you were indeed correct about having pathlib installed in my environment.  I removed it on both my windows and linux systems and can confirm that this fixed my specific issue.  Many thanks for the tip.

Resolving patch:
diff --git a/pipenv/_compat.py b/pipenv/_compat.py
--- a/pipenv/_compat.py
+++ b/pipenv/_compat.py
@@ -29,10 +29,13 @@ def _infer_return_type(*args):
                 _types.add(type(arg))
         return _types.pop()
 
-try:
-    from pathlib import Path
-except ImportError:
-    from pathlib2 import Path
+if sys.version_info[:2] >= (3, 5):
+    try:
+        from pathlib import Path
+    except ImportError:
+        from .vendor.pathlib2 import Path
+else:
+    from .vendor.pathlib2 import Path
 
 
 try:
diff --git a/pipenv/project.py b/pipenv/project.py
--- a/pipenv/project.py
+++ b/pipenv/project.py
@@ -14,10 +14,7 @@
 import toml
 import json as simplejson
 
-try:
-    from pathlib import Path
-except ImportError:
-    from pathlib2 import Path
+from ._compat import Path
 
 from .cmdparse import Script
 from .vendor.requirementslib import Requirement
diff --git a/pipenv/utils.py b/pipenv/utils.py
--- a/pipenv/utils.py
+++ b/pipenv/utils.py
@@ -14,7 +14,6 @@
 
 from click import echo as click_echo
 from first import first
-
 try:
     from weakref import finalize
 except ImportError:
@@ -36,13 +35,7 @@ def detach(self):
     from urllib.parse import urlparse
 except ImportError:
     from urlparse import urlparse
-try:
-    from pathlib import Path
-except ImportError:
-    try:
-        from .vendor.pathlib2 import Path
-    except ImportError:
-        pass
+
 from distutils.spawn import find_executable
 from contextlib import contextmanager
 from .pep508checker import lookup
@@ -609,6 +602,7 @@ def is_installable_file(path):
     from .patched.notpip._internal.utils.misc import is_installable_dir
     from .patched.notpip._internal.utils.packaging import specifiers
     from .patched.notpip._internal.download import is_archive_file
+    from ._compat import Path
 
     if hasattr(path, 'keys') and any(
         key for key in path.keys() if key in ['file', 'path']
@@ -853,6 +847,7 @@ def find_windows_executable(bin_path, exe_name):
 
 
 def path_to_url(path):
+    from ._compat import Path
     return Path(normalize_drive(os.path.abspath(path))).as_uri()
 
 
@@ -1158,7 +1153,7 @@ def get_vcs_deps(
     pypi_mirror=None,
 ):
     from .patched.notpip._internal.vcs import VcsSupport
-    from ._compat import TemporaryDirectory
+    from ._compat import TemporaryDirectory, Path
 
     section = "vcs_dev_packages" if dev else "vcs_packages"
     reqs = []


Test patch:
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -4,18 +4,12 @@
 
 import pytest
 
-from pipenv._compat import TemporaryDirectory
+from pipenv._compat import TemporaryDirectory, Path
 from pipenv.vendor import delegator
 from pipenv.vendor import requests
 from pipenv.vendor import six
 from pipenv.vendor import toml
 
-try:
-    from pathlib import Path
-except ImportError:
-    from pipenv.vendor.pathlib2 import Path
-
-
 if six.PY2:
     class ResourceWarning(Warning):
         pass
diff --git a/tests/integration/test_install_basic.py b/tests/integration/test_install_basic.py
--- a/tests/integration/test_install_basic.py
+++ b/tests/integration/test_install_basic.py
@@ -2,13 +2,9 @@
 import os
 
 from pipenv.utils import temp_environ
-from pipenv._compat import TemporaryDirectory
+from pipenv._compat import TemporaryDirectory, Path
 from pipenv.vendor import delegator
 from pipenv.project import Project
-try:
-    from pathlib import Path
-except ImportError:
-    from pipenv.vendor.pathlib2 import Path
 
 import pytest
 
diff --git a/tests/integration/test_install_twists.py b/tests/integration/test_install_twists.py
--- a/tests/integration/test_install_twists.py
+++ b/tests/integration/test_install_twists.py
@@ -1,10 +1,7 @@
 import os
 import shutil
 from pipenv.project import Project
-try:
-    import pathlib
-except ImportError:
-    import pathlib2 as pathlib
+from pipenv._compat import Path
 
 from pipenv.utils import mkdir_p, temp_environ
 
@@ -223,7 +220,7 @@ def test_relative_paths(PipenvInstance, pypi, testsroot):
         dep = p.pipfile['packages'][key]
 
         assert 'path' in dep
-        assert pathlib.Path('.', artifact_dir, file_name) == pathlib.Path(dep['path'])
+        assert Path('.', artifact_dir, file_name) == Path(dep['path'])
         assert c.return_code == 0
 
 
diff --git a/tests/integration/test_install_uri.py b/tests/integration/test_install_uri.py
--- a/tests/integration/test_install_uri.py
+++ b/tests/integration/test_install_uri.py
@@ -2,10 +2,7 @@
 import os
 from flaky import flaky
 import delegator
-try:
-    from pathlib import Path
-except ImportError:
-    from pathlib2 import Path
+from pipenv._compat import Path
 
 
 @pytest.mark.vcs
diff --git a/tests/integration/test_pipenv.py b/tests/integration/test_pipenv.py
--- a/tests/integration/test_pipenv.py
+++ b/tests/integration/test_pipenv.py
@@ -11,11 +11,7 @@
 from pipenv.core import activate_virtualenv
 from pipenv.project import Project
 from pipenv.vendor import delegator
-
-try:
-    from pathlib import Path
-except ImportError:
-    from pipenv.vendor.pathlib2 import Path
+from pipenv._compat import Path
 
 
 @pytest.mark.code
@@ -123,4 +119,4 @@ def mocked_mkdtemp(suffix, prefix, dir):
             assert os.path.isdir(venv_path)
             # Manually clean up environment, since PipenvInstance assumes that
             # the virutalenv is in the project directory.
-            p.pipenv('--rm')
\ No newline at end of file
+            p.pipenv('--rm')
diff --git a/tests/integration/test_windows.py b/tests/integration/test_windows.py
--- a/tests/integration/test_windows.py
+++ b/tests/integration/test_windows.py
@@ -1,7 +1,7 @@
 import os
 
 from pipenv.project import Project
-from pipenv.vendor import pathlib2 as pathlib
+from pipenv._compat import Path
 
 import pytest
 
@@ -33,7 +33,7 @@ def test_case_changes_windows(PipenvInstance, pypi):
 @pytest.mark.files
 def test_local_path_windows(PipenvInstance, pypi):
     whl = (
-        pathlib.Path(__file__).parent.parent
+        Path(__file__).parent.parent
         .joinpath('pypi', 'six', 'six-1.11.0-py2.py3-none-any.whl')
     )
     try:
@@ -48,7 +48,7 @@ def test_local_path_windows(PipenvInstance, pypi):
 @pytest.mark.files
 def test_local_path_windows_forward_slash(PipenvInstance, pypi):
     whl = (
-        pathlib.Path(__file__).parent.parent
+        Path(__file__).parent.parent
         .joinpath('pypi', 'six', 'six-1.11.0-py2.py3-none-any.whl')
     )
     try: