Repository: qutip/qutip

Issue:
tarball from PyPI requires git
**Describe the bug**
Source tarball taken from PyPI https://pypi.org/project/qutip/ fails without git:
```
===>  Configuring for py38-qutip-4.6.0
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "setup.py", line 231, in <module>
    options = process_options()
  File "setup.py", line 45, in process_options
    options = _determine_version(options)
  File "setup.py", line 127, in _determine_version
    git_out = subprocess.run(
  File "/usr/local/lib/python3.8/subprocess.py", line 493, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/local/lib/python3.8/subprocess.py", line 858, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/local/lib/python3.8/subprocess.py", line 1704, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'git'
```

git shouldn't be required because PyPI != GitHub and it is expected to be just a directory and not a cloned git repository.



Discussion:
Ah, this is the confluence of two bugs, unfortunately.  As an immediate workaround, you should be able to set the environment variable `CI_QUTIP_RELEASE=1` before running `pip` (assuming it propagates the env), and it should build without an error.

The two bugs:
1. building in "release mode" isn't the default; on CI to build the wheels we set a release-mode flag, but the sdist can't tell.  That's what the environment variable above does.  I'll have to think a little about how we'll do versioning.  This bug slipped in in #1465, where the primary concern was getting wheels built, and I didn't notice that the sdist wouldn't build in release mode.
2. `git` isn't meant to be required - in dev mode, it should just get to the string "nogit". That error is actually within a `try/except` block, except it only catches `subprocess.SubprocessError`, when it should catch both `subprocess.SubprocessError` and `OSError`.

Thanks for the report.

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -10,6 +10,7 @@
 import warnings
 
 # Required third-party imports, must be specified in pyproject.toml.
+import packaging.version
 from setuptools import setup, Extension
 import distutils.sysconfig
 import numpy as np
@@ -49,15 +50,9 @@ def process_options():
 
 def _determine_user_arguments(options):
     """
-    Add the 'release' and 'openmp' options to the collection, based on the
-    passed command-line arguments or environment variables.
+    Add the 'openmp' option to the collection, based on the passed command-line
+    arguments or environment variables.
     """
-    options['release'] = (
-        '--release' in sys.argv
-        or bool(os.environ.get('CI_QUTIP_RELEASE'))
-    )
-    if '--release' in sys.argv:
-        sys.argv.remove('--release')
     options['openmp'] = (
         '--with-openmp' in sys.argv
         or bool(os.environ.get('CI_QUTIP_WITH_OPENMP'))
@@ -104,7 +99,7 @@ def _determine_compilation_options(options):
 
 def _determine_version(options):
     """
-    Adds the 'short_version' and 'version' options.
+    Adds the 'short_version', 'version' and 'release' options.
 
     Read from the VERSION file to discover the version.  This should be a
     single line file containing valid Python package public identifier (see PEP
@@ -117,12 +112,16 @@ def _determine_version(options):
     """
     version_filename = os.path.join(options['rootdir'], 'VERSION')
     with open(version_filename, "r") as version_file:
-        version = options['short_version'] = version_file.read().strip()
-    VERSION_RE = r'\d+(\.\d+)*((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?'
-    if re.fullmatch(VERSION_RE, version, re.A) is None:
-        raise ValueError("invalid version: " + version)
+        version_string = version_file.read().strip()
+    version = packaging.version.parse(version_string)
+    if isinstance(version, packaging.version.LegacyVersion):
+        raise ValueError("invalid version: " + version_string)
+    options['short_version'] = str(version.public)
+    options['release'] = not version.is_devrelease
     if not options['release']:
-        version += "+"
+        # Put the version string into canonical form, if it wasn't already.
+        version_string = str(version)
+        version_string += "+"
         try:
             git_out = subprocess.run(
                 ('git', 'rev-parse', '--verify', '--short=7', 'HEAD'),
@@ -130,10 +129,14 @@ def _determine_version(options):
                 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
             )
             git_hash = git_out.stdout.decode(sys.stdout.encoding).strip()
-            version += git_hash or "nogit"
-        except subprocess.CalledProcessError:
-            version += "nogit"
-    options['version'] = version
+            version_string += git_hash or "nogit"
+        # CalledProcessError is for if the git command fails for internal
+        # reasons (e.g. we're not in a git repository), OSError is for if
+        # something goes wrong when trying to run git (e.g. it's not installed,
+        # or a permission error).
+        except (subprocess.CalledProcessError, OSError):
+            version_string += "nogit"
+    options['version'] = version_string
     return options
 
 


Test patch: