Skip to content

Commit cbf3310

Browse files
committed
--update-all
Part of #63
1 parent ccb661b commit cbf3310

File tree

3 files changed

+282
-8
lines changed

3 files changed

+282
-8
lines changed

hashin.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,8 @@
4040
parser = argparse.ArgumentParser()
4141
parser.add_argument(
4242
"packages",
43-
help=(
44-
"One or more package specifiers (e.g. some-package or " "some-package==1.2.3)"
45-
),
46-
nargs="+",
43+
help="One or more package specifiers (e.g. some-package or some-package==1.2.3)",
44+
nargs="*",
4745
)
4846
parser.add_argument(
4947
"-r",
@@ -79,6 +77,13 @@
7977
action="store_true",
8078
default=False,
8179
)
80+
parser.add_argument(
81+
"-u",
82+
"--update-all",
83+
help="Update all mentioned packages in the requirements file.",
84+
action="store_true",
85+
default=False,
86+
)
8287

8388

8489
major_pip_version = int(pip_api.version().split(".")[0])
@@ -119,12 +124,22 @@ def _download(url, binary=False):
119124
return r.read().decode(encoding)
120125

121126

122-
def run(specs, *args, **kwargs):
127+
def run(specs, requirements_file, *args, **kwargs):
128+
if not specs: # then, assume all in the requirements file
129+
regex = re.compile(r"(^|\n|\n\r).*==")
130+
specs = []
131+
with open(requirements_file) as f:
132+
for line in f:
133+
if regex.search(line):
134+
req = Requirement(line.split("\\")[0])
135+
# Deliberately strip the specifier (aka. the version)
136+
req.specifier = None
137+
specs.append(str(req))
123138
if isinstance(specs, str):
124139
specs = [specs]
125140

126141
for spec in specs:
127-
run_single_package(spec, *args, **kwargs)
142+
run_single_package(spec, requirements_file, *args, **kwargs)
128143
return 0
129144

130145

@@ -198,12 +213,11 @@ def run_single_package(
198213

199214

200215
def amend_requirements_content(requirements, package, new_lines):
201-
# if the package wasn't already there, add it to the bottom
202216
regex = re.compile(
203217
r"(^|\n|\n\r){0}==|(^|\n|\n\r){0}\[.*\]==".format(re.escape(package)),
204218
re.IGNORECASE,
205219
)
206-
220+
# if the package wasn't already there, add it to the bottom
207221
if not regex.search(requirements):
208222
# easy peasy
209223
if requirements:
@@ -493,6 +507,18 @@ def main():
493507

494508
args = parser.parse_args()
495509

510+
if args.update_all:
511+
if args.packages:
512+
print(
513+
"Can not combine the --update-all option with a list of packages.",
514+
file=sys.stderr,
515+
)
516+
return 2
517+
elif not args.packages:
518+
print("If you don't use --update-all you must list packages.", file=sys.stderr)
519+
parser.print_usage()
520+
return 3
521+
496522
try:
497523
return run(
498524
args.packages,

tests/test_arg_parse.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def test_everything():
2727
version=False,
2828
include_prereleases=False,
2929
dry_run=True,
30+
update_all=False,
3031
)
3132
assert args == (expected, [])
3233

@@ -55,6 +56,7 @@ def test_everything_long():
5556
version=False,
5657
include_prereleases=False,
5758
dry_run=True,
59+
update_all=False,
5860
)
5961
assert args == (expected, [])
6062

@@ -70,5 +72,6 @@ def test_minimal():
7072
version=False,
7173
include_prereleases=False,
7274
dry_run=False,
75+
update_all=False,
7376
)
7477
assert args == (expected, [])

tests/test_cli.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import argparse
12
import sys
23
import json
34
import os
@@ -187,11 +188,71 @@ def test_main_packageerrors_stderr(self, mock_run, mock_sys, mock_parser):
187188
# Doesn't matter so much what, just make sure it breaks
188189
mock_run.side_effect = hashin.PackageError("Some message here")
189190

191+
def mock_parse_args(*a, **k):
192+
return argparse.Namespace(
193+
packages=["something"],
194+
requirements_file="requirements.txt",
195+
algorithm="sha256",
196+
python_version="3.8",
197+
verbose=False,
198+
include_prereleases=False,
199+
dry_run=False,
200+
update_all=False,
201+
)
202+
203+
mock_parser.parse_args.side_effect = mock_parse_args
204+
190205
error = hashin.main()
191206
self.assertEqual(error, 1)
192207
mock_sys.stderr.write.assert_any_call("Some message here")
193208
mock_sys.stderr.write.assert_any_call("\n")
194209

210+
@mock.patch("hashin.parser")
211+
@mock.patch("hashin.sys")
212+
def test_packages_and_update_all(self, mock_sys, mock_parser):
213+
def mock_parse_args(*a, **k):
214+
return argparse.Namespace(
215+
packages=["something"],
216+
requirements_file="requirements.txt",
217+
algorithm="sha256",
218+
python_version="3.8",
219+
verbose=False,
220+
include_prereleases=False,
221+
dry_run=False,
222+
update_all=True, # Note!
223+
)
224+
225+
mock_parser.parse_args.side_effect = mock_parse_args
226+
227+
error = hashin.main()
228+
self.assertEqual(error, 2)
229+
mock_sys.stderr.write.assert_any_call(
230+
"Can not combine the --update-all option with a list of packages."
231+
)
232+
233+
@mock.patch("hashin.parser")
234+
@mock.patch("hashin.sys")
235+
def test_no_packages_and_not_update_all(self, mock_sys, mock_parser):
236+
def mock_parse_args(*a, **k):
237+
return argparse.Namespace(
238+
packages=[], # Note!
239+
requirements_file="requirements.txt",
240+
algorithm="sha256",
241+
python_version="3.8",
242+
verbose=False,
243+
include_prereleases=False,
244+
dry_run=False,
245+
update_all=False,
246+
)
247+
248+
mock_parser.parse_args.side_effect = mock_parse_args
249+
250+
error = hashin.main()
251+
self.assertEqual(error, 3)
252+
mock_sys.stderr.write.assert_any_call(
253+
"If you don't use --update-all you must list packages."
254+
)
255+
195256
@mock.patch("hashin.sys")
196257
def test_main_version(self, mock_sys):
197258
mock_sys.argv = [None, "--version"]
@@ -602,6 +663,190 @@ def test_run_case_insensitive(self, murlopen):
602663
it should find it and correct the cast typing per what it is
603664
inside the PyPI data."""
604665

666+
def mocked_get(url, **options):
667+
# if url == "https://pypi.org/pypi/HASHin/json":
668+
# return _Response(
669+
# "",
670+
# status_code=301,
671+
# headers={"location": "https://pypi.org/pypi/hashin/json"},
672+
# )
673+
# elif url == "https://pypi.org/pypi/hashIN/json":
674+
# return _Response(
675+
# "",
676+
# status_code=301,
677+
# headers={"location": "https://pypi.org/pypi/hashin/json"},
678+
# )
679+
if url == "https://pypi.org/pypi/hashin/json":
680+
return _Response(
681+
{
682+
"info": {"version": "0.11", "name": "hashin"},
683+
"releases": {
684+
"0.11": [
685+
{
686+
"url": "https://pypi.org/packages/source/p/hashin/hashin-0.11.tar.gz",
687+
"digests": {"sha256": "bbbbb"},
688+
}
689+
],
690+
"0.10": [
691+
{
692+
"url": "https://pypi.org/packages/source/p/hashin/hashin-0.10.tar.gz",
693+
"digests": {"sha256": "aaaaa"},
694+
}
695+
],
696+
},
697+
}
698+
)
699+
elif url == "https://pypi.org/pypi/hashin/json":
700+
return _Response(
701+
{
702+
"info": {"version": "0.11", "name": "hashin"},
703+
"releases": {
704+
"0.11": [
705+
{
706+
"url": "https://pypi.org/packages/source/p/hashin/hashin-0.11.tar.gz",
707+
"digests": {"sha256": "bbbbb"},
708+
}
709+
],
710+
"0.10": [
711+
{
712+
"url": "https://pypi.org/packages/source/p/hashin/hashin-0.10.tar.gz",
713+
"digests": {"sha256": "aaaaa"},
714+
}
715+
],
716+
},
717+
}
718+
)
719+
elif url == "https://pypi.org/pypi/requests/json":
720+
return _Response(
721+
{
722+
"info": {"version": "1.2.4", "name": "requests"},
723+
"releases": {
724+
"1.2.4": [
725+
{
726+
"url": "https://pypi.org/packages/source/p/requests/requests-1.2.4.tar.gz",
727+
"digests": {"sha256": "dededede"},
728+
}
729+
]
730+
},
731+
}
732+
)
733+
if url == "https://pypi.org/pypi/enum34/json":
734+
return _Response(
735+
{
736+
"info": {"version": "1.1.6", "name": "enum34"},
737+
"releases": {
738+
"1.1.6": [
739+
{
740+
"has_sig": False,
741+
"upload_time": "2016-05-16T03:31:13",
742+
"comment_text": "",
743+
"python_version": "py2",
744+
"url": "https://pypi.org/packages/c5/db/enum34-1.1.6-py2-none-any.whl",
745+
"digests": {
746+
"md5": "68f6982cc07dde78f4b500db829860bd",
747+
"sha256": "aaaaa",
748+
},
749+
"md5_digest": "68f6982cc07dde78f4b500db829860bd",
750+
"downloads": 4297423,
751+
"filename": "enum34-1.1.6-py2-none-any.whl",
752+
"packagetype": "bdist_wheel",
753+
"path": "c5/db/enum34-1.1.6-py2-none-any.whl",
754+
"size": 12427,
755+
},
756+
{
757+
"has_sig": False,
758+
"upload_time": "2016-05-16T03:31:19",
759+
"comment_text": "",
760+
"python_version": "py3",
761+
"url": "https://pypi.org/packages/af/42/enum34-1.1.6-py3-none-any.whl",
762+
"md5_digest": "a63ecb4f0b1b85fb69be64bdea999b43",
763+
"digests": {
764+
"md5": "a63ecb4f0b1b85fb69be64bdea999b43",
765+
"sha256": "bbbbb",
766+
},
767+
"downloads": 98598,
768+
"filename": "enum34-1.1.6-py3-none-any.whl",
769+
"packagetype": "bdist_wheel",
770+
"path": "af/42/enum34-1.1.6-py3-none-any.whl",
771+
"size": 12428,
772+
},
773+
{
774+
"has_sig": False,
775+
"upload_time": "2016-05-16T03:31:30",
776+
"comment_text": "",
777+
"python_version": "source",
778+
"url": "https://pypi.org/packages/bf/3e/enum34-1.1.6.tar.gz",
779+
"md5_digest": "5f13a0841a61f7fc295c514490d120d0",
780+
"digests": {
781+
"md5": "5f13a0841a61f7fc295c514490d120d0",
782+
"sha256": "ccccc",
783+
},
784+
"downloads": 188090,
785+
"filename": "enum34-1.1.6.tar.gz",
786+
"packagetype": "sdist",
787+
"path": "bf/3e/enum34-1.1.6.tar.gz",
788+
"size": 40048,
789+
},
790+
{
791+
"has_sig": False,
792+
"upload_time": "2016-05-16T03:31:48",
793+
"comment_text": "",
794+
"python_version": "source",
795+
"url": "https://pypi.org/packages/e8/26/enum34-1.1.6.zip",
796+
"md5_digest": "61ad7871532d4ce2d77fac2579237a9e",
797+
"digests": {
798+
"md5": "61ad7871532d4ce2d77fac2579237a9e",
799+
"sha256": "dddddd",
800+
},
801+
"downloads": 775920,
802+
"filename": "enum34-1.1.6.zip",
803+
"packagetype": "sdist",
804+
"path": "e8/26/enum34-1.1.6.zip",
805+
"size": 44773,
806+
},
807+
]
808+
},
809+
}
810+
)
811+
812+
raise NotImplementedError(url)
813+
814+
murlopen.side_effect = mocked_get
815+
816+
with tmpfile() as filename:
817+
with self.assertRaises(FileNotFoundError):
818+
retcode = hashin.run(None, filename, "sha256")
819+
820+
with open(filename, "w") as f:
821+
f.write("# This is comment. Ignore this.\n")
822+
f.write("\n")
823+
f.write("requests[security]==1.2.3 \\\n")
824+
f.write(" --hash=sha256:99dcfdaae\n")
825+
f.write("hashin==0.11 \\\n")
826+
f.write(" --hash=sha256:a84b8c9ab623\n")
827+
f.write("enum34==1.1.5; python_version <= '3.4' \\\n")
828+
f.write(" --hash=sha256:12ce5c2ef718\n")
829+
f.write("\n")
830+
831+
retcode = hashin.run(None, filename, "sha256", verbose=True)
832+
833+
self.assertEqual(retcode, 0)
834+
with open(filename) as f:
835+
output = f.read()
836+
837+
self.assertTrue("requests[security]==1.2.3" not in output)
838+
self.assertTrue("requests[security]==1.2.4" in output)
839+
# This one didn't need to be updated.
840+
self.assertTrue("hashin==0.11" in output)
841+
self.assertTrue('enum34==1.1.5; python_version <= "3.4"' not in output)
842+
self.assertTrue('enum34==1.1.6; python_version <= "3.4"' in output)
843+
844+
@cleanup_tmpdir("hashin*")
845+
@mock.patch("hashin.urlopen")
846+
def test_run_update_all(self, murlopen):
847+
"""The --update-all flag will extra all the names from the existing
848+
requirements file, and check with pypi.org if there's a new version."""
849+
605850
def mocked_get(url, **options):
606851
if url == "https://pypi.org/pypi/HAShin/json":
607852
return _Response(

0 commit comments

Comments
 (0)