|
14 | 14 | import os |
15 | 15 | import re |
16 | 16 | import sys |
| 17 | +from typing import NamedTuple |
17 | 18 | import zipfile |
18 | 19 | from configparser import ConfigParser |
19 | 20 | from pathlib import Path |
|
23 | 24 | import pip_requirements_parser |
24 | 25 | import pkginfo2 |
25 | 26 | from commoncode import fileutils |
| 27 | +from packaging.specifiers import SpecifierSet |
26 | 28 | from packageurl import PackageURL |
27 | 29 | from packaging import markers |
28 | 30 | from packaging.requirements import Requirement |
@@ -453,8 +455,9 @@ def parse_metadata(location, datasource_id, package_type): |
453 | 455 | type=package_type, |
454 | 456 | primary_language='Python', |
455 | 457 | name=name, |
456 | | - version=version, #TODO: https://github.com/nexB/scancode-toolkit/issues/3014 |
457 | | - description=get_description(meta, str(location)), |
| 458 | + version=version, |
| 459 | + description=get_description( metainfo= meta, location= str(location)), |
| 460 | + #TODO: https://github.com/nexB/scancode-toolkit/issues/3014 |
458 | 461 | declared_license=get_declared_license(meta), |
459 | 462 | keywords=get_keywords(meta), |
460 | 463 | parties=get_parties(meta), |
@@ -646,6 +649,14 @@ def parse(cls, location): |
646 | 649 | ) |
647 | 650 |
|
648 | 651 |
|
| 652 | +class ResolvedPurl(NamedTuple): |
| 653 | + """ |
| 654 | + A resolved PURL |
| 655 | + """ |
| 656 | + purl: PackageURL |
| 657 | + is_resolved: bool |
| 658 | + |
| 659 | + |
649 | 660 | class BaseDependencyFileHandler(BasePypiHandler): |
650 | 661 | """ |
651 | 662 | Base class for a dependency files parsed with the same library |
@@ -690,8 +701,35 @@ def parse(cls, location): |
690 | 701 | parser.read_file(f) |
691 | 702 | for section in parser.values(): |
692 | 703 | if section.name == 'options': |
693 | | - reqs = list(get_requirement_from_section(section=section, sub_section="install_requires")) |
694 | | - dependent_packages.extend(cls.parse_reqs(reqs, "install")) |
| 704 | + scope_by_sub_section = { |
| 705 | + "install_requires": "install", |
| 706 | + "tests_require": "test", |
| 707 | + "setup_requires": "setup", |
| 708 | + "python_requires": "python", |
| 709 | + } |
| 710 | + for sub_section in scope_by_sub_section: |
| 711 | + if sub_section not in section: |
| 712 | + continue |
| 713 | + scope = scope_by_sub_section[sub_section] |
| 714 | + if scope != "python": |
| 715 | + reqs = list(get_requirement_from_section(section=section, sub_section=sub_section)) |
| 716 | + dependent_packages.extend(cls.parse_reqs(reqs, scope)) |
| 717 | + continue |
| 718 | + python_requires = section[sub_section] |
| 719 | + purl = PackageURL( |
| 720 | + name="python", |
| 721 | + type="generic" |
| 722 | + ) |
| 723 | + resolved_purl = is_purl_resolved(purl = purl, specifiers= SpecifierSet(python_requires)) |
| 724 | + dependent_packages.append(models.DependentPackage( |
| 725 | + purl=str(resolved_purl.purl), |
| 726 | + scope=scope, |
| 727 | + is_runtime=True, |
| 728 | + is_optional=False, |
| 729 | + is_resolved=resolved_purl.is_resolved, |
| 730 | + extracted_requirement=f"python_requires{python_requires}", |
| 731 | + )) |
| 732 | + |
695 | 733 | if section.name == "options.extras_require": |
696 | 734 | for sub_section in section: |
697 | 735 | reqs = list(get_requirement_from_section(section=section, sub_section=sub_section)) |
@@ -742,28 +780,39 @@ def parse_reqs(cls, reqs, scope): |
742 | 780 | """ |
743 | 781 | dependent_packages = [] |
744 | 782 | for req in reqs: |
745 | | - is_resolved = False |
746 | 783 | req_parsed = packaging.requirements.Requirement(str(req)) |
747 | 784 | name = canonicalize_name(req_parsed.name) |
748 | 785 | purl = PackageURL(type="pypi", name=name) |
749 | 786 | specifiers = req_parsed.specifier._specs |
750 | | - if len(specifiers) == 1: |
751 | | - specifier = list(specifiers)[0] |
752 | | - if specifier.operator in ('==', '==='): |
753 | | - is_resolved = True |
754 | | - purl = purl._replace(version=specifier.version) |
| 787 | + resolved_purl = is_purl_resolved(purl = purl, specifiers= specifiers) |
755 | 788 | dependent_packages.append( |
756 | 789 | models.DependentPackage( |
757 | | - purl=str(purl), |
| 790 | + purl=str(resolved_purl.purl), |
758 | 791 | scope=scope, |
759 | 792 | is_runtime=True, |
760 | 793 | is_optional=False, |
761 | | - is_resolved=is_resolved, |
| 794 | + is_resolved=resolved_purl.is_resolved, |
762 | 795 | extracted_requirement=req |
763 | 796 | ) |
764 | 797 | ) |
765 | 798 | return dependent_packages |
766 | 799 |
|
| 800 | + |
| 801 | +def is_purl_resolved(purl: PackageURL, specifiers: SpecifierSet): |
| 802 | + """ |
| 803 | + Check if the purl is resolved |
| 804 | + """ |
| 805 | + is_resolved = False |
| 806 | + if len(specifiers) == 1: |
| 807 | + specifier = list(specifiers)[0] |
| 808 | + if specifier.operator in ('==', '==='): |
| 809 | + is_resolved = True |
| 810 | + purl = purl._replace(version=specifier.version) |
| 811 | + return ResolvedPurl( |
| 812 | + purl=purl, |
| 813 | + is_resolved=is_resolved, |
| 814 | + ) |
| 815 | + |
767 | 816 | class PipfileHandler(BaseDependencyFileHandler): |
768 | 817 | datasource_id = 'pipfile' |
769 | 818 | path_patterns = ('*Pipfile',) |
@@ -1934,20 +1983,19 @@ def get_requirement_from_section(section, sub_section): |
1934 | 1983 | """ |
1935 | 1984 | content = section.get(sub_section) or "" |
1936 | 1985 | for req in content.splitlines(): |
1937 | | - if req: |
1938 | | - #pytest-mypy >= 0.9.1; \ |
1939 | | - req = req.replace("; \\", "") |
1940 | | - # pip>=19.1 # For proper file:// URLs support. |
1941 | | - if "#" in req: |
1942 | | - req , _ = req.rsplit("#") |
| 1986 | + if not req: |
| 1987 | + continue |
| 1988 | + #pytest-mypy >= 0.9.1; \ |
| 1989 | + req = req.replace("; \\", "") |
| 1990 | + # pip>=19.1 # For proper file:// URLs support. |
| 1991 | + if "#" in req: |
| 1992 | + req , _ = req.rsplit("#") |
| 1993 | + try: |
| 1994 | + Requirement(req) |
| 1995 | + yield req |
| 1996 | + except: |
1943 | 1997 | #pure-eval; black; tox |
1944 | 1998 | req_split_by_semi_colon = req.split(";") |
1945 | 1999 | req_split_by_semi_colon = [req.strip() for req in req_split_by_semi_colon if req] |
1946 | | - if len(req_split_by_semi_colon) >= 2 and not(req_split_by_semi_colon[1].startswith("python_version") # pip>=19.1 ;python_version > 3.7 |
1947 | | - or req_split_by_semi_colon[1].startswith("sys_platform") # pip>=19.1 ;sys_platform = "Windows" |
1948 | | - or req_split_by_semi_colon[1].startswith("platform_system") # pip>=19.1 ;platform_system = "Windows" |
1949 | | - or req_split_by_semi_colon[1].startswith("platform_python_implementation")):#pytest-black>=0.3.7; platform_python_implementation != "PyPy" |
1950 | | - for temp_req in req_split_by_semi_colon: |
1951 | | - yield temp_req |
1952 | | - else: |
| 2000 | + for req in req_split_by_semi_colon: |
1953 | 2001 | yield req |
0 commit comments