Skip to content

Commit

Permalink
allow extras syntax (#83)
Browse files Browse the repository at this point in the history
* allow extras syntax

Fixes #70

* use Requirement().str() instead
  • Loading branch information
Peter Bengtsson authored Oct 25, 2018
1 parent bd4ab46 commit ccb661b
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 7 deletions.
7 changes: 6 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,12 @@ put it directly into ``pip``.
Version History
===============

0.13.x
next

* Support for "extras syntax". E.g. ``hashin "requests[security]"``. Doesn't
actually get hashes for ``security`` (in this case, that's not even a
package) but allows that syntax into your ``requirements.txt`` file.
See https://github.com/peterbe/hashin/issues/70

* All code is now formatted with `Black <https://pypi.org/project/black/>`_.

Expand Down
25 changes: 19 additions & 6 deletions hashin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import print_function
import argparse
import cgi
import difflib
import tempfile
import os
import re
Expand All @@ -14,8 +15,8 @@
from itertools import chain

import pip_api
from packaging.requirements import Requirement
from packaging.version import parse
import difflib

if sys.version_info >= (3,):
from urllib.request import urlopen
Expand Down Expand Up @@ -146,18 +147,26 @@ def run_single_package(
package, version = spec, None
# There are other ways to what the latest version is.

req = Requirement(package)

data = get_package_hashes(
package=package,
package=req.name,
version=version,
verbose=verbose,
python_versions=python_versions,
algorithm=algorithm,
include_prereleases=include_prereleases,
)
package = data["package"]
# We need to keep this `req` instance for the sake of turning it into a string
# the correct way. But, the name might actually be wrong. Suppose the user
# asked for "Django" but on PyPI it's actually called "django", then we want
# correct that.
# We do that by modifying only the `name` part of the `Requirement` instance.
req.name = package

maybe_restriction = "" if not restriction else "; {0}".format(restriction)
new_lines = "{0}=={1}{2} \\\n".format(package, data["version"], maybe_restriction)
new_lines = "{0}=={1}{2} \\\n".format(req, data["version"], maybe_restriction)
padding = " " * 4
for i, release in enumerate(data["hashes"]):
new_lines += "{0}--hash={1}:{2}".format(padding, algorithm, release["hash"])
Expand Down Expand Up @@ -190,8 +199,12 @@ def run_single_package(

def amend_requirements_content(requirements, package, new_lines):
# if the package wasn't already there, add it to the bottom
regex = "(^|\n|\n\r){0}==".format(re.escape(package))
if not re.search(regex, requirements, re.IGNORECASE):
regex = re.compile(
r"(^|\n|\n\r){0}==|(^|\n|\n\r){0}\[.*\]==".format(re.escape(package)),
re.IGNORECASE,
)

if not regex.search(requirements):
# easy peasy
if requirements:
requirements = requirements.strip() + "\n"
Expand All @@ -201,7 +214,7 @@ def amend_requirements_content(requirements, package, new_lines):
lines = []
padding = " " * 4
for line in requirements.splitlines():
if line.lower().startswith("{0}==".format(package.lower())):
if regex.search(line):
lines.append(line)
elif lines and line.startswith(padding):
lines.append(line)
Expand Down
182 changes: 182 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1186,3 +1186,185 @@ def mocked_get(url, **options):
self.assertRaises(
hashin.PackageError, hashin.get_package_hashes, package="uggamugga"
)

@cleanup_tmpdir("hashin*")
@mock.patch("hashin.urlopen")
def test_with_extras_syntax(self, murlopen):
"""When you want to add the hashes of a package by using the
"extras notation". E.g `requests[security]`.
In this case, it should basically ignore the `[security]` part when
doing the magic but get that back when putting the final result
into the requirements file.
"""

def mocked_get(url, **options):
if url == "https://pypi.org/pypi/hashin/json":
return _Response(
{
"info": {"version": "0.10", "name": "hashin"},
"releases": {
"0.10": [
{
"url": "https://pypi.org/packages/source/p/hashin/hashin-0.10.tar.gz",
"digests": {"sha256": "ccccc"},
}
]
},
}
)
raise NotImplementedError(url)

murlopen.side_effect = mocked_get

with tmpfile() as filename:
with open(filename, "w") as f:
f.write("")

retcode = hashin.run("hashin[stuff]", filename, "sha256")

self.assertEqual(retcode, 0)
with open(filename) as f:
output = f.read()
self.assertTrue("hashin[stuff]==0.10" in output)

@cleanup_tmpdir("hashin*")
@mock.patch("hashin.urlopen")
def test_extras_syntax_edit(self, murlopen):
def mocked_get(url, **options):
if url == "https://pypi.org/pypi/hashin/json":
return _Response(
{
"info": {"version": "0.10", "name": "hashin"},
"releases": {
"0.10": [
{
"url": "https://pypi.org/packages/source/p/hashin/hashin-0.10.tar.gz",
"digests": {"sha256": "ccccc"},
}
]
},
}
)
raise NotImplementedError(url)

murlopen.side_effect = mocked_get

with tmpfile() as filename:
with open(filename, "w") as f:
f.write("hashin==0.10\n")
f.write(" --hash=sha256:ccccc\n")

retcode = hashin.run("hashin[stuff]", filename, "sha256")

self.assertEqual(retcode, 0)
with open(filename) as f:
output = f.read()
self.assertTrue("hashin[stuff]==0.10" in output)
self.assertTrue("hashin==0.10" not in output)

@cleanup_tmpdir("hashin*")
@mock.patch("hashin.urlopen")
def test_add_extra_extras_syntax_edit(self, murlopen):
def mocked_get(url, **options):
if url == "https://pypi.org/pypi/hashin/json":
return _Response(
{
"info": {"version": "0.10", "name": "hashin"},
"releases": {
"0.10": [
{
"url": "https://pypi.org/packages/source/p/hashin/hashin-0.10.tar.gz",
"digests": {"sha256": "ccccc"},
}
]
},
}
)
raise NotImplementedError(url)

murlopen.side_effect = mocked_get

with tmpfile() as filename:
with open(filename, "w") as f:
f.write("hashin[stuff]==0.10\n")
f.write(" --hash=sha256:ccccc\n")

retcode = hashin.run("hashin[extra,stuff]", filename, "sha256")

self.assertEqual(retcode, 0)
with open(filename) as f:
output = f.read()
self.assertTrue("hashin[extra,stuff]==0.10" in output)
self.assertTrue("hashin==0.10" not in output)
self.assertTrue("hashin[stuff]==0.10" not in output)
self.assertTrue("hashin[extra]==0.10" not in output)

@cleanup_tmpdir("hashin*")
@mock.patch("hashin.urlopen")
def test_change_extra_extras_syntax_edit(self, murlopen):
def mocked_get(url, **options):
if url == "https://pypi.org/pypi/hashin/json":
return _Response(
{
"info": {"version": "0.10", "name": "hashin"},
"releases": {
"0.10": [
{
"url": "https://pypi.org/packages/source/p/hashin/hashin-0.10.tar.gz",
"digests": {"sha256": "ccccc"},
}
]
},
}
)
raise NotImplementedError(url)

murlopen.side_effect = mocked_get

with tmpfile() as filename:
with open(filename, "w") as f:
f.write("hashin[stuff]==0.10\n")
f.write(" --hash=sha256:ccccc\n")

retcode = hashin.run("hashin[different]", filename, "sha256")

self.assertEqual(retcode, 0)
with open(filename) as f:
output = f.read()
self.assertTrue("hashin[different]==0.10" in output)
self.assertTrue("hashin[stuff]==0.10" not in output)

@cleanup_tmpdir("hashin*")
@mock.patch("hashin.urlopen")
def test_remove_extra_extras_syntax_edit(self, murlopen):
def mocked_get(url, **options):
if url == "https://pypi.org/pypi/hashin/json":
return _Response(
{
"info": {"version": "0.10", "name": "hashin"},
"releases": {
"0.10": [
{
"url": "https://pypi.org/packages/source/p/hashin/hashin-0.10.tar.gz",
"digests": {"sha256": "ccccc"},
}
]
},
}
)
raise NotImplementedError(url)

murlopen.side_effect = mocked_get

with tmpfile() as filename:
with open(filename, "w") as f:
f.write("hashin[stuff]==0.10\n")
f.write(" --hash=sha256:ccccc\n")

retcode = hashin.run("hashin", filename, "sha256")

self.assertEqual(retcode, 0)
with open(filename) as f:
output = f.read()
self.assertTrue("hashin==0.10" in output)
self.assertTrue("hashin[stuff]==0.10" not in output)

0 comments on commit ccb661b

Please sign in to comment.