Repository: pypa/setuptools

Issue:
Add option to package build to disable normalization of version identifier
Originally reported by: **mklein0 (Bitbucket: [mklein0](http://bitbucket.org/mklein0), GitHub: [mklein0](http://github.com/mklein0))**

---

Could an option be added to package builds to disable the use of normalized version identifiers in the final package file name?

Under previous versions of setup tools a version identifier of `20140510.003458` would generate a package name of:

```
dated_release-20140510.003458-cp27-none-macosx_10_9_intel.whl
```

After setuptools 8:

```
dated_release-20140510.3458-cp27-none-macosx_10_9_intel.whl
```

My primary concern is for older installation tool/devpy versions, the package version matching will fail if I start using the new normalized package names. And I do not want to have to test it.

Aesthetics is another aspect as in this case, the version identifier is actually an encoding of an ISO8601 timestamp.:

```
date -u +'%Y%m%d.%H%M%S'
```

This request is derived from issue #302

---
- Bitbucket: https://bitbucket.org/pypa/setuptools/issue/308



Discussion:
I also noticed that, if I set the version number as, say, 1.2.3-rc1, setuptools normalizes it to 1.2.3rc1. This created several problems for me when generating debian packages. Allowing such kinds of normalization do be disabled would be a major improvement.

Would also like to be able to disable normalization, 1.2.3-rc1 being normalized to 1.2.3rc1 is causing problems for packaging for me as well.

@dstufft, do you have any opinion on this? IIRC, it was your contribution that added the normalization. What would be the ramifications of allowing bypassing of this normalization?

@jaraco I think as long as you still validate that it is a PEP 440 version (or warn if not.. I don't think we have it set to hard fail), not doing the normalization shouldn't be a big deal. I think my only reason to normalize was just to get everyone using the normal forms as the default since that makes things a bit saner I think. However allowing a flag to bypass normalization should not have any negative affect.
Thanks @dstufft. Glad to hear this wouldn't be a blocker. @mklein0 or others, would you consider putting together a pull request?
Hi there, I was wondering if there was a status update or further discussion on this issue.  I've just run into this when our CI pipeline built a new package (which appends a short git has to the version string) was changed to a different hash.

```
Version is: 2.4.5.5513c055
/usr/local/lib/python2.7/site-packages/setuptools/dist.py:332: UserWarning: Normalizing '2.4.5.5513c055' to '2.4.5.5513rc55'

```

It would seem to my quick analysis that there is an overly greedy regex munging version strings...
Not nice, but a workaround that works for me:

add this to your `setup.py`

`import pkg_resources`
`pkg_resources.extern.packaging.version.Version = pkg_resources.SetuptoolsLegacyVersion`

Just ran into this myself. I've already disabled the PEP 440 warnings and now I have to work around this also :(

We're packaging pre-releases following the semver.org standard, entirely for internal use only. I can maybe understand PyPI rejecting "internal only" versions at registration/upload time, but rejecting it outside of a public arena seems a little heavy-handed.

~My solution was to modify our version strings from "2.2.0-a046027" to "2.2.0-pre.a046027" - I couldn't figure a reasonable way to disable the normalization code through monkey-patching.~

I am using this monkeypatch to disable the normalization:

    from setuptools.extern.packaging import version
    version.Version = version.LegacyVersion

I threw together a quick proof of concept for adding a ``normalize_version=False`` parameter to ``setup()`` that would allow a project to disable version normalization. It does *not* prevent the warning about non PEP 440 versions (because it is true that using those versions are likely to cause issues), but it does provide a path for people who need a very specific version string.

That branch can be found at https://github.com/pypa/setuptools/compare/dont-normalize, it's only two lines of code plus re-indenting a block of code to be guarded by an if statement. I don't have time right now to develop it further, it would presumably require some tests and someone to shepherd it through the contribution process, but if someone does have the time, that code there is working.
Thanks, Donald!
@dstufft do you need a hand to make this a bona fide PR ?
I doubt I'm going to have the time to finish up the work that's needed to make that proof of concept into a real PR. Feel free to take it and run with it.
@dstufft code has changed a little since but this patch (even updated for the latest code) does not seem to help correctly: for instance I cannot create a valid semver version such as `1.1.0-alpha1` .... it will be normalized at all times as `1.1.0a1` for an sdist and a bdist_wheel which is no longer semver compliant.  At least the sdist should be left untouched IMHO. 
Will you support this feature in the next release? Any plans if next release don't support the feature? Thank you. 
@an-sush if you feel this is "unfair", would you consider submitting a patch then to help right this unfair wrong? :innocent:  ;)
I created https://github.com/ccrisan/setup-no-ver-normalize to "fix" this issue in my projects.
@ccrisan that's simple and clean. Monkey patching really allows awesome things
Is there any update on this issue? I would like to use -alpha prefixes in my versioning as suggested by semver standard. @ccrisan  your library works for the most part but converts - to _ in the wheel file.
> I threw together a quick proof of concept for adding a `normalize_version=False` parameter to `setup()`

I took a look at this, and while it's a good proof of concept (thanks Donald), I think I'd prefer an implementation that avoided a boolean parameter alongside the version parameter, but instead have some API where the version specified carries with it the fact that it shouldn't be normalized, either by way of wrapping it in an alternate class (such as ccrisan's `NoNormalizeVersion`) or by setting an attribute on the `str` that's passed in or by accepting a function (by name or value) for normalizing.

On the other hand, I'd also like something that if it's officially supported, would also work with declarative config. I'm considering this approach:

- a user could supply a version as `version='1.2..3 (sic)'`, where the ` (sic)` appended [acknowledges the non-normal usage](https://en.wikipedia.org/wiki/Sic).
- if `(sic)` is present, setuptools will strip the indicator and skip normalization.

While I sort-of like this approach, it also causes the value to be passed in-band (in the same string as the intended value), which I dislike.

I'm also considering:

- Setuptools would supply a `sic()` function, which if called on a string would indicate to Setuptools not to use normalization. e.g. `version=setuptools.sic('1.2..3')`.
- This approach would not be as portable to declarative config.

Thoughts?
> > I threw together a quick proof of concept for adding a `normalize_version=False` parameter to `setup()`
> 
> I took a look at this, and while it's a good proof of concept (thanks Donald), I think I'd prefer an implementation that avoided a boolean parameter alongside the version parameter, but instead have some API where the version specified carries with it the fact that it shouldn't be normalized, either by way of wrapping it in an alternate class (such as ccrisan's `NoNormalizeVersion`) or by setting an attribute on the `str` that's passed in or by accepting a function (by name or value) for normalizing.
> 
> On the other hand, I'd also like something that if it's officially supported, would also work with declarative config. I'm considering this approach:
> 
> * a user could supply a version as `version='1.2..3 (sic)'`, where the ` (sic)` appended [acknowledges the non-normal usage](https://en.wikipedia.org/wiki/Sic).
> * if `(sic)` is present, setuptools will strip the indicator and skip normalization.
> 
> While I sort-of like this approach, it also causes the value to be passed in-band (in the same string as the intended value), which I dislike.
> 
> I'm also considering:
> 
> * Setuptools would supply a `sic()` function, which if called on a string would indicate to Setuptools not to use normalization. e.g. `version=setuptools.sic('1.2..3')`.
> * This approach would not be as portable to declarative config.
> 
> Thoughts?

How about providing an option to pass an implementation for the normalize function when calling setup, if parameter is not passed then you use the default implementation, if passed as None then you return the version as it is. You could also pass a custom implementation if you like.
@KishanKishore you wrote:
> How about providing an option to pass an implementation for the normalize function when calling setup, if parameter is not passed then you use the default implementation, if passed as None then you return the version as it is. You could also pass a custom implementation if you like.

can you elaborate and be more explicit and also include an example?

Resolving patch:
diff --git a/setuptools/__init__.py b/setuptools/__init__.py
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -224,5 +224,9 @@ def findall(dir=os.curdir):
     return list(files)
 
 
+class sic(str):
+    """Treat this string as-is (https://en.wikipedia.org/wiki/Sic)"""
+
+
 # Apply monkey patches
 monkey.patch_all()
diff --git a/setuptools/dist.py b/setuptools/dist.py
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -30,6 +30,7 @@
 
 from . import SetuptoolsDeprecationWarning
 
+import setuptools
 from setuptools import windows_support
 from setuptools.monkey import get_unpatched
 from setuptools.config import parse_configuration
@@ -438,9 +439,22 @@ def __init__(self, attrs=None):
                 value = default() if default else None
             setattr(self.metadata, option, value)
 
-        self.metadata.version = self._validate_version(self.metadata.version)
+        self.metadata.version = self._normalize_version(
+            self._validate_version(self.metadata.version))
         self._finalize_requires()
 
+    @staticmethod
+    def _normalize_version(version):
+        if isinstance(version, setuptools.sic) or version is None:
+            return version
+
+        normalized = str(packaging.version.Version(version))
+        if version != normalized:
+            tmpl = "Normalizing '{version}' to '{normalized}'"
+            warnings.warn(tmpl.format(**locals()))
+            return normalized
+        return version
+
     @staticmethod
     def _validate_version(version):
         if isinstance(version, numbers.Number):
@@ -449,16 +463,7 @@ def _validate_version(version):
 
         if version is not None:
             try:
-                ver = packaging.version.Version(version)
-                normalized_version = str(ver)
-                if version != normalized_version:
-                    warnings.warn(
-                        "Normalizing '%s' to '%s'" % (
-                            version,
-                            normalized_version,
-                        )
-                    )
-                    version = normalized_version
+                packaging.version.Version(version)
             except (packaging.version.InvalidVersion, TypeError):
                 warnings.warn(
                     "The version specified (%r) is an invalid version, this "
@@ -466,6 +471,7 @@ def _validate_version(version):
                     "setuptools, pip, and PyPI. Please see PEP 440 for more "
                     "details." % version
                 )
+                return setuptools.sic(version)
         return version
 
     def _finalize_requires(self):


Test patch:
diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py
--- a/setuptools/tests/test_dist.py
+++ b/setuptools/tests/test_dist.py
@@ -12,6 +12,7 @@
     check_package_data,
     DistDeprecationWarning,
 )
+from setuptools import sic
 from setuptools import Distribution
 from setuptools.extern.six.moves.urllib.request import pathname2url
 from setuptools.extern.six.moves.urllib_parse import urljoin
@@ -133,6 +134,10 @@ def __read_test_cases():
             name='foo',
             version='1.0.0',
         )),
+        ('Bypass normalized version', dict(
+            name='foo',
+            version=sic('1.0.0a'),
+        )),
     ]
 
     return test_cases