diff --git a/src/flake8_requirements/checker.py b/src/flake8_requirements/checker.py index 7e8c0e1..8519b14 100644 --- a/src/flake8_requirements/checker.py +++ b/src/flake8_requirements/checker.py @@ -418,15 +418,32 @@ def discover_project_root_dir(cls): break root_dir = os.path.abspath(os.path.join(root_dir, "..")) + _requirement_match_option = re.compile( + r"(-[\w-]+)(.*)").match + + _requirement_match_spec = re.compile( + r"(.*?)\s+--(global-option|install-option|hash)").match + + _requirement_match_archive = re.compile( + r"(.*)(\.(tar(\.(bz2|gz|lz|lzma|xz))?|tbz|tgz|tlz|txz|whl|zip))", + re.IGNORECASE).match + _requirement_match_archive_spec = re.compile( + r"(\w+)(-[^-]+)?").match + + _requirement_match_vcs = re.compile( + r"(git|hg|svn|bzr)\+(.*)").match + _requirement_match_vcs_spec = re.compile( + r".*egg=([\w\-\.]+)").match + @classmethod def resolve_requirement(cls, requirement, max_depth=0, path=None): """Resolves flags like -r in an individual requirement line.""" option = None - option_matcher = re.match(r"(-[\w-]+)(.*)", requirement) - if option_matcher: - option = option_matcher.group(1) - requirement = option_matcher.group(2).lstrip() + option_match = cls._requirement_match_option(requirement) + if option_match is not None: + option = option_match.group(1) + requirement = option_match.group(2).lstrip() if option in ("-e", "--editable"): # We do not care about installation mode. @@ -449,8 +466,31 @@ def resolve_requirement(cls, requirement, max_depth=0, path=None): # Skip whole line if option was not processed earlier. return [] - # Extract requirement name (skip comments, options, etc.). - return [re.split(r"[^\w.-]+", requirement, 1)[0]] + # Check for a requirement given as a VSC link. + vcs_match = cls._requirement_match_vcs(requirement) + vcs_spec_match = cls._requirement_match_vcs_spec( + vcs_match.group(2) if vcs_match is not None else "") + if vcs_spec_match is not None: + return [vcs_spec_match.group(1)] + + # Check for a requirement given as a local archive file. + archive_ext_match = cls._requirement_match_archive(requirement) + if archive_ext_match is not None: + base = os.path.basename(archive_ext_match.group(1)) + archive_spec_match = cls._requirement_match_archive_spec(base) + if archive_spec_match is not None: + name, version = archive_spec_match.groups() + return [ + name if not version else + "{} == {}".format(name, version[1:]) + ] + + # Extract requirement specifier (skip in-line options). + spec_match = cls._requirement_match_spec(requirement) + if spec_match is not None: + requirement = spec_match.group(1) + + return [requirement.strip()] @classmethod @memoize diff --git a/test/test_requirements.py b/test/test_requirements.py index 7ee20d4..013c943 100644 --- a/test/test_requirements.py +++ b/test/test_requirements.py @@ -1,3 +1,4 @@ +import os import unittest from collections import OrderedDict @@ -39,13 +40,13 @@ def setUp(self): def test_resolve_requirement(self): self.assertEqual( Flake8Checker.resolve_requirement("foo >= 1.0.0"), - ["foo"], + ["foo >= 1.0.0"], ) def test_resolve_requirement_with_option(self): self.assertEqual( - Flake8Checker.resolve_requirement("foo-bar.v1==1.0 --option"), - ["foo-bar.v1"], + Flake8Checker.resolve_requirement("foo-bar.v1==1.0 --hash=md5:."), + ["foo-bar.v1==1.0"], ) def test_resolve_requirement_standalone_option(self): @@ -72,7 +73,7 @@ def test_resolve_requirement_with_file_content(self): )))): self.assertEqual( Flake8Checker.resolve_requirement("-r requirements.txt", 1), - ["foo", "bar"], + ["foo >= 1.0.0", "bar <= 1.0.0"], ) def test_resolve_requirement_with_file_content_line_continuation(self): @@ -81,7 +82,7 @@ def test_resolve_requirement_with_file_content_line_continuation(self): )))): self.assertEqual( Flake8Checker.resolve_requirement("-r requirements.txt", 1), - ["foo"], + ["foo[bar] >= 1.0.0"], ) def test_resolve_requirement_with_file_content_line_continuation_2(self): @@ -90,7 +91,7 @@ def test_resolve_requirement_with_file_content_line_continuation_2(self): )))): self.assertEqual( Flake8Checker.resolve_requirement("-r requirements.txt", 1), - ["foo", "bar"], + ["foo >= 1.0.0", "bar"], ) def test_resolve_requirement_with_file_recursion_beyond_max_depth(self): @@ -107,7 +108,7 @@ def test_resolve_requirement_with_file_recursion(self): )))): self.assertEqual( Flake8Checker.resolve_requirement("-r requirements.txt", 2), - ["baz", "qux", "bar"], + ["baz", "qux", "bar <= 1.0.0"], ) def test_resolve_requirement_with_relative_include(self): @@ -147,7 +148,7 @@ def test_init_with_user_requirements(self): self.assertEqual( checker.get_requirements_txt(), tuple(parse_requirements([ - "foo", + "foo >= 1.0.0", "bar", ])), ) @@ -166,8 +167,8 @@ def test_init_with_simple_requirements(self): self.assertEqual( checker.get_requirements_txt(), tuple(parse_requirements([ - "foo", - "bar", + "foo >= 1.0.0", + "bar <= 1.0.0", ])), ) @@ -193,9 +194,34 @@ def test_init_with_recursive_requirements(self): self.assertEqual( checker.get_requirements_txt(), tuple(parse_requirements([ - "foo", + "foo >= 1.0.0", "baz", "qux", - "bar", + "bar <= 1.0.0", + ])), + ) + + def test_init_misc(self): + curdir = os.path.abspath(os.path.dirname(__file__)) + with open(os.path.join(curdir, "test_requirements.txt")) as f: + requirements_content = f.read() + with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict(( + ("requirements.txt", requirements_content), + )))): + checker = Flake8Checker(None, None) + self.assertEqual( + checker.get_requirements_txt(), + tuple(parse_requirements([ + "nose", + "apache == 0.6.9", + "coverage[graph,test] ~= 3.1", + "graph <2.0, >=1.2 ; python_version < '3.8'", + "foo-project >= 1.2", + "bar-project == 8.8", + "configuration", + "blackBox == 1.4.4", + "exPackage_paint == 1.4.8.dev1984+49a8814", + "package-one", + "package-two", ])), ) diff --git a/test/test_requirements.txt b/test/test_requirements.txt new file mode 100644 index 0000000..3de30a8 --- /dev/null +++ b/test/test_requirements.txt @@ -0,0 +1,27 @@ +####### requirements.txt ####### + +###### Requirements without Version Specifiers ###### +nose + +###### Requirements with Version Specifiers ###### +apache == 0.6.9 # Version Matching. Must be version 0.6.9 +coverage[test, graph] ~= 3.1 # Compatible release. Same as >= 3.1, == 3.* +graph >=1.2, <2.0 ; python_version < '3.8' + +###### Global options ###### +--find-links http://some.archives.com/archives +--no-index + +###### Requirements with in-line options ###### +foo-project >= 1.2 --install-option="--prefix=/usr/local --no-compile" +bar-project == 8.8 --hash=sha256:cecb534b7d0022683d030b048a2d679c6ff3df969fd7b847027f1ed8d739ac8c \ + --hash=md5:a540092b44178949e8d63ddd7a74f95d + +###### Requirements from a particular file ###### +/opt/configuration.tar.gz # Local configuration +/opt/blackBox-1.4.4-cp34-none-win32.whl # Local proprietary package +http://example.com/snapshot-builds/exPackage_paint-1.4.8.dev1984+49a8814-cp34-none-win_amd64.whl + +###### Requirements from a VCS ###### +git+git://github.com/path/to/package-one@releases/tag/v3.1.4#egg=package-one +git+git://github.com/path/to/package-two@master#egg=package-two&subdirectory=src