dependabot-python 0.367.0 → 0.369.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/build +4 -0
  3. data/helpers/requirements.txt +1 -0
  4. data/helpers/test/fixtures/no_dependencies.toml +3 -0
  5. data/helpers/test/fixtures/pep621_arbitrary_equality.toml +7 -0
  6. data/helpers/test/fixtures/pep621_dependencies.toml +21 -0
  7. data/helpers/test/fixtures/pep621_empty_deps.toml +8 -0
  8. data/helpers/test/fixtures/pep621_extras.toml +8 -0
  9. data/helpers/test/fixtures/pep621_markers.toml +7 -0
  10. data/helpers/test/fixtures/pep621_multiple_extras.toml +7 -0
  11. data/helpers/test/fixtures/pep621_no_version.toml +8 -0
  12. data/helpers/test/fixtures/pep621_only_build_system.toml +3 -0
  13. data/helpers/test/fixtures/pep735_cycle.toml +13 -0
  14. data/helpers/test/fixtures/pep735_dependency_groups.toml +18 -0
  15. data/helpers/test/fixtures/requirements/constraints.txt +1 -0
  16. data/helpers/test/fixtures/requirements/markers.txt +1 -0
  17. data/helpers/test/fixtures/requirements/requirements-dev.txt +2 -0
  18. data/helpers/test/fixtures/requirements/requirements.txt +5 -0
  19. data/helpers/test/fixtures/requirements/with_constraints.txt +2 -0
  20. data/helpers/test/fixtures/requirements_empty/.gitkeep +0 -0
  21. data/helpers/test/fixtures/setup_cfg/setup.cfg +16 -0
  22. data/helpers/test/fixtures/setup_py/setup.py +20 -0
  23. data/helpers/test/fixtures/setup_py_comments/setup.py +9 -0
  24. data/helpers/test/test_hasher.py +114 -0
  25. data/helpers/test/test_parse_requirements.py +103 -0
  26. data/helpers/test/test_parse_setup.py +127 -0
  27. data/helpers/test/test_parser.py +184 -0
  28. data/helpers/test/test_run.py +49 -0
  29. data/lib/dependabot/python/file_updater/poetry_file_updater.rb +16 -11
  30. data/lib/dependabot/python/file_updater/pyproject_preparer.rb +109 -0
  31. data/lib/dependabot/python/file_updater/requirement_file_updater.rb +36 -13
  32. data/lib/dependabot/python/file_updater/requirement_replacer.rb +29 -21
  33. data/lib/dependabot/python/metadata_finder.rb +152 -20
  34. data/lib/dependabot/python/shared_file_fetcher.rb +10 -5
  35. data/lib/dependabot/python/update_checker/latest_version_finder.rb +4 -2
  36. metadata +29 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e54cc0660ba9d6007661da639e66151ac7cdab9dafb2ea460ee544b825e7790
4
- data.tar.gz: eaaec236cd949ffa2a446b5056a56378a2bcf954eb32da78e0ec0cb9f7b0bb4c
3
+ metadata.gz: 64cdd01e07bc2f6472a1027884006af4c7df3648168f04135c7b91cdf5530f14
4
+ data.tar.gz: b5610dc0420858f42cb23a8462abcd7ebd4b9b1aab8be3ca28233a99c239982c
5
5
  SHA512:
6
- metadata.gz: 88f0d4fc4032adab9165c2d0187ef56660ebc219472723ad97c61c128e764cfeac49d4a49abfa4999f05ac85f9ccee3189dec442fc5701bf1afd41d18d706fba
7
- data.tar.gz: 931b2880cdfda0184098b61416963e02542a13993752f83e8682f3c25b7de3e2f6c45affe5a80e231a331fc6bcd4b62b0eec3bab33aae065a92d70932e31c844
6
+ metadata.gz: f4cb556708bba8d54dc3d04495d021e4808cf5651184ed9fe2d1b65d6b1114c688f92d9be8ab8fae6e96d818643ce4c4824db5971dc2fabf55370b6ffff26f39
7
+ data.tar.gz: 64514c6fad289e34af6bdbaeecc25f8ff815ba193d77088a86fa3399c9379ab1cad740ff0f18ae0ac176e85375714e15f36ceae638b96d6e2714f425ce86cd96
data/helpers/build CHANGED
@@ -17,6 +17,10 @@ cp -r \
17
17
  "$helpers_dir/requirements.txt" \
18
18
  "$install_dir"
19
19
 
20
+ if [ -d "$helpers_dir/test" ]; then
21
+ cp -r "$helpers_dir/test" "$install_dir"
22
+ fi
23
+
20
24
  cd "$install_dir"
21
25
  PYENV_VERSION=$1 pyenv exec pip3 --disable-pip-version-check install --use-pep517 -r "requirements.txt"
22
26
 
@@ -5,6 +5,7 @@ hashin==1.0.5
5
5
  pipenv==2024.4.1
6
6
  plette==2.1.0
7
7
  poetry==2.2.1
8
+ pytest==8.3.5
8
9
  # TODO: Replace 3p package `tomli` with 3.11's new stdlib `tomllib` once we drop support for Python 3.10.
9
10
  tomli==2.2.1
10
11
 
@@ -0,0 +1,3 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
@@ -0,0 +1,7 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
4
+
5
+ dependencies = [
6
+ "numpy===1.24.0rc1",
7
+ ]
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
4
+ requires-python = ">=3.10"
5
+
6
+ dependencies = [
7
+ "requests>=2.13.0,<3.0",
8
+ "urllib3==1.26.0",
9
+ ]
10
+
11
+ [project.optional-dependencies]
12
+ socks = [
13
+ "PySocks >= 1.5.6, != 1.5.7, < 2",
14
+ ]
15
+ tests = [
16
+ "pytest >= 7.0, < 8",
17
+ ]
18
+
19
+ [build-system]
20
+ requires = ["setuptools>=68.0"]
21
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
4
+
5
+ dependencies = []
6
+
7
+ [project.optional-dependencies]
8
+ dev = []
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
4
+ requires-python = ">=3.10"
5
+
6
+ dependencies = [
7
+ "cachecontrol[filecache]>=0.14.0",
8
+ ]
@@ -0,0 +1,7 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
4
+
5
+ dependencies = [
6
+ "requests>=2.13.0; python_version >= '3.8'",
7
+ ]
@@ -0,0 +1,7 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
4
+
5
+ dependencies = [
6
+ "boto3[crt,s3]>=1.28.0",
7
+ ]
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
4
+
5
+ dependencies = [
6
+ "requests",
7
+ "flask",
8
+ ]
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
4
+
5
+ [dependency-groups]
6
+ a = [
7
+ {include-group = "b"},
8
+ "requests>=2.0",
9
+ ]
10
+ b = [
11
+ {include-group = "a"},
12
+ "flask>=2.0",
13
+ ]
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "myapp"
3
+ version = "1.0.0"
4
+ requires-python = ">=3.10"
5
+
6
+ dependencies = [
7
+ "requests>=2.13.0",
8
+ ]
9
+
10
+ [dependency-groups]
11
+ dev = [
12
+ "pytest==7.1.3",
13
+ "black==22.10.0",
14
+ ]
15
+ lint = [
16
+ {include-group = "dev"},
17
+ "flake8>=5.0",
18
+ ]
@@ -0,0 +1 @@
1
+ requests<3.0
@@ -0,0 +1 @@
1
+ pywin32>=1.0; sys_platform == "win32"
@@ -0,0 +1,2 @@
1
+ pytest>=7.0
2
+ black==22.10.0
@@ -0,0 +1,5 @@
1
+ # Basic requirements file
2
+ requests>=2.13.0,<3.0
3
+ urllib3==1.26.0
4
+ Flask[async]>=2.0
5
+ boto3 # AWS SDK
@@ -0,0 +1,2 @@
1
+ -c constraints.txt
2
+ requests>=2.13.0
File without changes
@@ -0,0 +1,16 @@
1
+ [metadata]
2
+ name = myapp
3
+ version = 1.0.0
4
+
5
+ [options]
6
+ install_requires =
7
+ requests>=2.13.0
8
+ urllib3==1.26.0
9
+ setup_requires =
10
+ setuptools>=68.0
11
+ tests_require =
12
+ pytest>=7.0
13
+
14
+ [options.extras_require]
15
+ socks =
16
+ PySocks>=1.5.6
@@ -0,0 +1,20 @@
1
+ from setuptools import setup
2
+
3
+ setup(
4
+ name="myapp",
5
+ version="1.0.0",
6
+ install_requires=[
7
+ "requests>=2.13.0",
8
+ "urllib3==1.26.0",
9
+ ],
10
+ setup_requires=[
11
+ "setuptools>=68.0",
12
+ ],
13
+ tests_require=[
14
+ "pytest>=7.0",
15
+ ],
16
+ extras_require={
17
+ "socks": ["PySocks>=1.5.6"],
18
+ "dev": ["black==22.10.0"],
19
+ },
20
+ )
@@ -0,0 +1,9 @@
1
+ from setuptools import setup
2
+
3
+ setup(
4
+ name="myapp",
5
+ version="1.0.0",
6
+ install_requires=[
7
+ "requests>=2.13.0 # HTTP library",
8
+ ],
9
+ )
@@ -0,0 +1,114 @@
1
+ import json
2
+ import os
3
+ import ssl
4
+ import sys
5
+ from unittest.mock import MagicMock, patch
6
+ from urllib.error import URLError
7
+
8
+ sys.path.insert(
9
+ 0, os.path.join(os.path.dirname(__file__), os.pardir, "lib")
10
+ )
11
+
12
+ import hasher # noqa: E402
13
+ import hashin as hashin_mod # noqa: E402
14
+
15
+
16
+ class TestGetDependencyHash:
17
+ @patch("hasher.hashin.get_package_hashes")
18
+ def test_returns_hashes(self, mock_get):
19
+ mock_get.return_value = {
20
+ "hashes": [
21
+ {"hash": "abc123", "platform": "linux"},
22
+ {"hash": "def456", "platform": "macos"},
23
+ ]
24
+ }
25
+
26
+ result = json.loads(hasher.get_dependency_hash(
27
+ "requests", "2.28.0", "sha256"
28
+ ))
29
+
30
+ assert "result" in result
31
+ assert len(result["result"]) == 2
32
+ assert result["result"][0]["hash"] == "abc123"
33
+ mock_get.assert_called_once()
34
+
35
+ @patch("hasher.hashin.get_package_hashes")
36
+ def test_custom_index_url(self, mock_get):
37
+ mock_get.return_value = {"hashes": []}
38
+
39
+ hasher.get_dependency_hash(
40
+ "requests", "2.28.0", "sha256",
41
+ index_url="https://custom.registry/simple/"
42
+ )
43
+
44
+ mock_get.assert_called_once_with(
45
+ "requests",
46
+ version="2.28.0",
47
+ algorithm="sha256",
48
+ index_url="https://custom.registry/simple/"
49
+ )
50
+
51
+ @patch("hasher.hashin.get_package_hashes")
52
+ def test_package_not_found(self, mock_get):
53
+ mock_get.side_effect = hashin_mod.PackageNotFoundError(
54
+ "no-such-package"
55
+ )
56
+
57
+ result = json.loads(hasher.get_dependency_hash(
58
+ "no-such-package", "1.0.0", "sha256"
59
+ ))
60
+
61
+ assert "error" in result
62
+
63
+ @patch("hasher.hashin.get_package_hashes")
64
+ def test_ssl_certificate_error(self, mock_get):
65
+ ssl_error = ssl.SSLError(
66
+ "CERTIFICATE_VERIFY_FAILED: unable to get local issuer"
67
+ )
68
+ mock_get.side_effect = URLError(ssl_error)
69
+
70
+ result = json.loads(hasher.get_dependency_hash(
71
+ "requests", "2.28.0", "sha256"
72
+ ))
73
+
74
+ assert "error" in result
75
+ assert "CERTIFICATE_VERIFY_FAILED" in result["error"]
76
+
77
+ @patch("hasher.hashin.get_package_hashes")
78
+ def test_non_ssl_url_error_raises(self, mock_get):
79
+ mock_get.side_effect = URLError("Connection refused")
80
+
81
+ try:
82
+ hasher.get_dependency_hash("requests", "2.28.0", "sha256")
83
+ assert False, "Expected URLError to be raised"
84
+ except URLError:
85
+ pass # expected
86
+
87
+
88
+ class TestGetPipfileHash:
89
+ @patch("builtins.open")
90
+ @patch("hasher.plette")
91
+ def test_returns_hash(self, mock_plette, mock_open):
92
+ mock_pipfile = MagicMock()
93
+ mock_pipfile.get_hash.return_value.value = "abc123hash"
94
+ mock_plette.Pipfile.load.return_value = mock_pipfile
95
+
96
+ result = json.loads(hasher.get_pipfile_hash("/tmp/project"))
97
+
98
+ assert result["result"] == "abc123hash"
99
+ mock_open.assert_called_once_with("/tmp/project/Pipfile")
100
+
101
+
102
+ class TestGetPyprojectHash:
103
+ @patch("hasher.Factory")
104
+ def test_returns_hash(self, mock_factory_cls):
105
+ mock_poetry = MagicMock()
106
+ mock_poetry.locker._get_content_hash.return_value = "xyz789hash"
107
+ mock_factory_cls.return_value.create_poetry.return_value = mock_poetry
108
+
109
+ result = json.loads(hasher.get_pyproject_hash("/tmp/project"))
110
+
111
+ assert result["result"] == "xyz789hash"
112
+ mock_factory_cls.return_value.create_poetry.assert_called_once_with(
113
+ "/tmp/project"
114
+ )
@@ -0,0 +1,103 @@
1
+ import json
2
+ import os
3
+ import sys
4
+
5
+ sys.path.insert(
6
+ 0, os.path.join(os.path.dirname(__file__), os.pardir, "lib")
7
+ )
8
+
9
+ from parser import parse_requirements # noqa: E402
10
+
11
+ FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures")
12
+
13
+
14
+ def parse(fixture_dir):
15
+ path = os.path.join(FIXTURES, fixture_dir)
16
+ result = json.loads(parse_requirements(path))
17
+ return result
18
+
19
+
20
+ def find_dep(deps, name):
21
+ return next((d for d in deps if d["name"] == name), None)
22
+
23
+
24
+ def find_dep_in_file(deps, name, file_substring):
25
+ return next(
26
+ (d for d in deps
27
+ if d["name"] == name and file_substring in d["file"]),
28
+ None
29
+ )
30
+
31
+
32
+ class TestParseRequirementsTxt:
33
+ def test_parses_basic_requirements(self):
34
+ result = parse("requirements")
35
+ deps = result["result"]
36
+ requests = find_dep_in_file(deps, "requests", "requirements.txt")
37
+ assert requests is not None
38
+ assert requests["requirement"] == "<3.0,>=2.13.0"
39
+
40
+ def test_parses_pinned_version(self):
41
+ result = parse("requirements")
42
+ deps = result["result"]
43
+ urllib3 = find_dep(deps, "urllib3")
44
+ assert urllib3 is not None
45
+ assert urllib3["version"] == "1.26.0"
46
+ assert urllib3["requirement"] == "==1.26.0"
47
+
48
+ def test_parses_extras(self):
49
+ result = parse("requirements")
50
+ deps = result["result"]
51
+ flask = find_dep(deps, "Flask")
52
+ assert flask is not None
53
+ assert flask["extras"] == ["async"]
54
+
55
+ def test_strips_inline_comments(self):
56
+ result = parse("requirements")
57
+ deps = result["result"]
58
+ boto3 = find_dep(deps, "boto3")
59
+ assert boto3 is not None
60
+ # boto3 has no version specifier, just a comment
61
+ assert boto3["requirement"] is None or boto3["requirement"] == ""
62
+
63
+ def test_file_path_is_relative(self):
64
+ result = parse("requirements")
65
+ deps = result["result"]
66
+ for dep in deps:
67
+ assert not os.path.isabs(dep["file"])
68
+
69
+ def test_parses_dev_requirements(self):
70
+ result = parse("requirements")
71
+ deps = result["result"]
72
+ black = find_dep(deps, "black")
73
+ assert black is not None
74
+ assert black["version"] == "22.10.0"
75
+ assert "requirements-dev.txt" in black["file"]
76
+
77
+ def test_parses_markers(self):
78
+ result = parse("requirements")
79
+ deps = result["result"]
80
+ pywin32 = find_dep(deps, "pywin32")
81
+ assert pywin32 is not None
82
+ assert pywin32["markers"] == 'sys_platform == "win32"'
83
+
84
+ def test_empty_directory_returns_empty(self):
85
+ result = parse("requirements_empty")
86
+ deps = result["result"]
87
+ assert deps == []
88
+
89
+ def test_constraint_file_deps(self):
90
+ """Requirements with -c constraints should still parse the deps."""
91
+ result = parse("requirements")
92
+ deps = result["result"]
93
+ # with_constraints.txt has requests
94
+ req_files = [d["file"] for d in deps if d["name"] == "requests"]
95
+ assert any("with_constraints" in f for f in req_files)
96
+
97
+ def test_multiple_files_parsed(self):
98
+ """All .txt files in the directory are parsed."""
99
+ result = parse("requirements")
100
+ deps = result["result"]
101
+ files = {d["file"] for d in deps}
102
+ assert any("requirements.txt" in f for f in files)
103
+ assert any("requirements-dev.txt" in f for f in files)
@@ -0,0 +1,127 @@
1
+ import json
2
+ import os
3
+ import sys
4
+
5
+ sys.path.insert(
6
+ 0, os.path.join(os.path.dirname(__file__), os.pardir, "lib")
7
+ )
8
+
9
+ from parser import parse_setup # noqa: E402
10
+
11
+ FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures")
12
+
13
+
14
+ def parse(fixture_dir):
15
+ path = os.path.join(FIXTURES, fixture_dir)
16
+ result = json.loads(parse_setup(path))
17
+ return result
18
+
19
+
20
+ def find_dep(deps, name):
21
+ return next((d for d in deps if d["name"] == name), None)
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # setup.py parsing
26
+ # ---------------------------------------------------------------------------
27
+ class TestSetupPy:
28
+ def test_parses_install_requires(self):
29
+ result = parse("setup_py")
30
+ deps = result["result"]
31
+ requests = find_dep(deps, "requests")
32
+ assert requests is not None
33
+ assert requests["requirement"] == ">=2.13.0"
34
+ assert requests["requirement_type"] == "install_requires"
35
+ assert requests["file"] == "setup.py"
36
+
37
+ def test_parses_pinned_version(self):
38
+ result = parse("setup_py")
39
+ deps = result["result"]
40
+ urllib3 = find_dep(deps, "urllib3")
41
+ assert urllib3 is not None
42
+ assert urllib3["version"] == "1.26.0"
43
+
44
+ def test_parses_setup_requires(self):
45
+ result = parse("setup_py")
46
+ deps = result["result"]
47
+ setuptools = find_dep(deps, "setuptools")
48
+ assert setuptools is not None
49
+ assert setuptools["requirement_type"] == "setup_requires"
50
+
51
+ def test_parses_tests_require(self):
52
+ result = parse("setup_py")
53
+ deps = result["result"]
54
+ pytest = find_dep(deps, "pytest")
55
+ assert pytest is not None
56
+ assert pytest["requirement_type"] == "tests_require"
57
+
58
+ def test_parses_extras_require(self):
59
+ result = parse("setup_py")
60
+ deps = result["result"]
61
+ pysocks = find_dep(deps, "PySocks")
62
+ assert pysocks is not None
63
+ assert pysocks["requirement_type"] == "extras_require:socks"
64
+
65
+ def test_parses_multiple_extras_groups(self):
66
+ result = parse("setup_py")
67
+ deps = result["result"]
68
+ extras = [d for d in deps if d["requirement_type"].startswith(
69
+ "extras_require:"
70
+ )]
71
+ groups = {d["requirement_type"] for d in extras}
72
+ assert "extras_require:socks" in groups
73
+ assert "extras_require:dev" in groups
74
+
75
+ def test_strips_comments(self):
76
+ result = parse("setup_py_comments")
77
+ deps = result["result"]
78
+ requests = find_dep(deps, "requests")
79
+ assert requests is not None
80
+ assert requests["requirement"] == ">=2.13.0"
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # setup.cfg parsing
85
+ # ---------------------------------------------------------------------------
86
+ class TestSetupCfg:
87
+ def test_parses_install_requires(self):
88
+ result = parse("setup_cfg")
89
+ deps = result["result"]
90
+ requests = find_dep(deps, "requests")
91
+ assert requests is not None
92
+ assert requests["requirement"] == ">=2.13.0"
93
+ assert requests["requirement_type"] == "install_requires"
94
+ assert requests["file"] == "setup.cfg"
95
+
96
+ def test_parses_pinned_version(self):
97
+ result = parse("setup_cfg")
98
+ deps = result["result"]
99
+ urllib3 = find_dep(deps, "urllib3")
100
+ assert urllib3 is not None
101
+ assert urllib3["version"] == "1.26.0"
102
+
103
+ def test_parses_setup_requires(self):
104
+ result = parse("setup_cfg")
105
+ deps = result["result"]
106
+ setuptools = find_dep(deps, "setuptools")
107
+ assert setuptools is not None
108
+ assert setuptools["requirement_type"] == "setup_requires"
109
+
110
+ def test_parses_tests_require(self):
111
+ result = parse("setup_cfg")
112
+ deps = result["result"]
113
+ pytest = find_dep(deps, "pytest")
114
+ assert pytest is not None
115
+ assert pytest["requirement_type"] == "tests_require"
116
+
117
+ def test_parses_extras_require(self):
118
+ result = parse("setup_cfg")
119
+ deps = result["result"]
120
+ pysocks = find_dep(deps, "PySocks")
121
+ assert pysocks is not None
122
+ assert pysocks["requirement_type"] == "extras_require:socks"
123
+
124
+ def test_empty_directory_returns_empty(self):
125
+ result = parse("requirements_empty")
126
+ deps = result["result"]
127
+ assert deps == []