Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
test_core_metadata.py551 linesDownload Raw Back to tests
1from __future__ import annotations2 3import functools4import io5from email import message_from_string6from email.generator import Generator7from email.message import EmailMessage8from email.parser import Parser9from email.policy import EmailPolicy10from inspect import cleandoc11from pathlib import Path12from unittest.mock import Mock13 14import jaraco.path15import pytest16from packaging.metadata import Metadata17 18from setuptools import sic19from setuptools._core_metadata import rfc822_escape, rfc822_unescape20from setuptools.config import expand, setupcfg21from setuptools.dist import Distribution22 23from .config.downloads import retrieve_file, urls_from_file24 25EXAMPLE_BASE_INFO = dict(26    name="package",27    version="0.0.1",28    author="Foo Bar",29    author_email="foo@bar.net",30    long_description="Long\ndescription",31    description="Short description",32    keywords=["one", "two"],33)34 35 36@pytest.mark.parametrize(37    ("content", "result"),38    (39        pytest.param(40            "Just a single line",41            None,42            id="single_line",43        ),44        pytest.param(45            "Multiline\nText\nwithout\nextra indents\n",46            None,47            id="multiline",48        ),49        pytest.param(50            "Multiline\n    With\n\nadditional\n  indentation",51            None,52            id="multiline_with_indentation",53        ),54        pytest.param(55            "  Leading whitespace",56            "Leading whitespace",57            id="remove_leading_whitespace",58        ),59        pytest.param(60            "  Leading whitespace\nIn\n    Multiline comment",61            "Leading whitespace\nIn\n    Multiline comment",62            id="remove_leading_whitespace_multiline",63        ),64    ),65)66def test_rfc822_unescape(content, result):67    assert (result or content) == rfc822_unescape(rfc822_escape(content))68 69 70def __read_test_cases():71    base = EXAMPLE_BASE_INFO72 73    params = functools.partial(dict, base)74 75    return [76        ('Metadata version 1.0', params()),77        (78            'Metadata Version 1.0: Short long description',79            params(80                long_description='Short long description',81            ),82        ),83        (84            'Metadata version 1.1: Classifiers',85            params(86                classifiers=[87                    'Programming Language :: Python :: 3',88                    'Programming Language :: Python :: 3.7',89                    'License :: OSI Approved :: MIT License',90                ],91            ),92        ),93        (94            'Metadata version 1.1: Download URL',95            params(96                download_url='https://example.com',97            ),98        ),99        (100            'Metadata Version 1.2: Requires-Python',101            params(102                python_requires='>=3.7',103            ),104        ),105        pytest.param(106            'Metadata Version 1.2: Project-Url',107            params(project_urls=dict(Foo='https://example.bar')),108            marks=pytest.mark.xfail(109                reason="Issue #1578: project_urls not read",110            ),111        ),112        (113            'Metadata Version 2.1: Long Description Content Type',114            params(115                long_description_content_type='text/x-rst; charset=UTF-8',116            ),117        ),118        (119            'License',120            params(121                license='MIT',122            ),123        ),124        (125            'License multiline',126            params(127                license='This is a long license \nover multiple lines',128            ),129        ),130        pytest.param(131            'Metadata Version 2.1: Provides Extra',132            params(provides_extras=['foo', 'bar']),133            marks=pytest.mark.xfail(reason="provides_extras not read"),134        ),135        (136            'Missing author',137            dict(138                name='foo',139                version='1.0.0',140                author_email='snorri@sturluson.name',141            ),142        ),143        (144            'Missing author e-mail',145            dict(146                name='foo',147                version='1.0.0',148                author='Snorri Sturluson',149            ),150        ),151        (152            'Missing author and e-mail',153            dict(154                name='foo',155                version='1.0.0',156            ),157        ),158        (159            'Bypass normalized version',160            dict(161                name='foo',162                version=sic('1.0.0a'),163            ),164        ),165    ]166 167 168@pytest.mark.parametrize(("name", "attrs"), __read_test_cases())169def test_read_metadata(name, attrs):170    dist = Distribution(attrs)171    metadata_out = dist.metadata172    dist_class = metadata_out.__class__173 174    # Write to PKG_INFO and then load into a new metadata object175    PKG_INFO = io.StringIO()176 177    metadata_out.write_pkg_file(PKG_INFO)178    PKG_INFO.seek(0)179    pkg_info = PKG_INFO.read()180    assert _valid_metadata(pkg_info)181 182    PKG_INFO.seek(0)183    metadata_in = dist_class()184    metadata_in.read_pkg_file(PKG_INFO)185 186    tested_attrs = [187        ('name', dist_class.get_name),188        ('version', dist_class.get_version),189        ('author', dist_class.get_contact),190        ('author_email', dist_class.get_contact_email),191        ('metadata_version', dist_class.get_metadata_version),192        ('provides', dist_class.get_provides),193        ('description', dist_class.get_description),194        ('long_description', dist_class.get_long_description),195        ('download_url', dist_class.get_download_url),196        ('keywords', dist_class.get_keywords),197        ('platforms', dist_class.get_platforms),198        ('obsoletes', dist_class.get_obsoletes),199        ('requires', dist_class.get_requires),200        ('classifiers', dist_class.get_classifiers),201        ('project_urls', lambda s: getattr(s, 'project_urls', {})),202        ('provides_extras', lambda s: getattr(s, 'provides_extras', {})),203    ]204 205    for attr, getter in tested_attrs:206        assert getter(metadata_in) == getter(metadata_out)207 208 209def __maintainer_test_cases():210    attrs = {"name": "package", "version": "1.0", "description": "xxx"}211 212    def merge_dicts(d1, d2):213        d1 = d1.copy()214        d1.update(d2)215 216        return d1217 218    return [219        ('No author, no maintainer', attrs.copy()),220        (221            'Author (no e-mail), no maintainer',222            merge_dicts(attrs, {'author': 'Author Name'}),223        ),224        (225            'Author (e-mail), no maintainer',226            merge_dicts(227                attrs, {'author': 'Author Name', 'author_email': 'author@name.com'}228            ),229        ),230        (231            'No author, maintainer (no e-mail)',232            merge_dicts(attrs, {'maintainer': 'Maintainer Name'}),233        ),234        (235            'No author, maintainer (e-mail)',236            merge_dicts(237                attrs,238                {239                    'maintainer': 'Maintainer Name',240                    'maintainer_email': 'maintainer@name.com',241                },242            ),243        ),244        (245            'Author (no e-mail), Maintainer (no-email)',246            merge_dicts(247                attrs, {'author': 'Author Name', 'maintainer': 'Maintainer Name'}248            ),249        ),250        (251            'Author (e-mail), Maintainer (e-mail)',252            merge_dicts(253                attrs,254                {255                    'author': 'Author Name',256                    'author_email': 'author@name.com',257                    'maintainer': 'Maintainer Name',258                    'maintainer_email': 'maintainer@name.com',259                },260            ),261        ),262        (263            'No author (e-mail), no maintainer (e-mail)',264            merge_dicts(265                attrs,266                {267                    'author_email': 'author@name.com',268                    'maintainer_email': 'maintainer@name.com',269                },270            ),271        ),272        ('Author unicode', merge_dicts(attrs, {'author': '鉄沢寛'})),273        ('Maintainer unicode', merge_dicts(attrs, {'maintainer': 'Jan Łukasiewicz'})),274    ]275 276 277@pytest.mark.parametrize(("name", "attrs"), __maintainer_test_cases())278def test_maintainer_author(name, attrs, tmpdir):279    tested_keys = {280        'author': 'Author',281        'author_email': 'Author-email',282        'maintainer': 'Maintainer',283        'maintainer_email': 'Maintainer-email',284    }285 286    # Generate a PKG-INFO file287    dist = Distribution(attrs)288    fn = tmpdir.mkdir('pkg_info')289    fn_s = str(fn)290 291    dist.metadata.write_pkg_info(fn_s)292 293    with open(str(fn.join('PKG-INFO')), 'r', encoding='utf-8') as f:294        pkg_info = f.read()295 296    assert _valid_metadata(pkg_info)297 298    # Drop blank lines and strip lines from default description299    raw_pkg_lines = pkg_info.splitlines()300    pkg_lines = list(filter(None, raw_pkg_lines[:-2]))301 302    pkg_lines_set = set(pkg_lines)303 304    # Duplicate lines should not be generated305    assert len(pkg_lines) == len(pkg_lines_set)306 307    for fkey, dkey in tested_keys.items():308        val = attrs.get(dkey, None)309        if val is None:310            for line in pkg_lines:311                assert not line.startswith(fkey + ':')312        else:313            line = f'{fkey}: {val}'314            assert line in pkg_lines_set315 316 317class TestParityWithMetadataFromPyPaWheel:318    def base_example(self):319        attrs = dict(320            **EXAMPLE_BASE_INFO,321            # Example with complex requirement definition322            python_requires=">=3.8",323            install_requires="""324            packaging==23.2325            more-itertools==8.8.0; extra == "other"326            jaraco.text==3.7.0327            importlib-resources==5.10.2; python_version<"3.8"328            importlib-metadata==6.0.0 ; python_version<"3.8"329            colorama>=0.4.4; sys_platform == "win32"330            """,331            extras_require={332                "testing": """333                    pytest >= 6334                    pytest-checkdocs >= 2.4335                    tomli ; \\336                            # Using stdlib when possible337                            python_version < "3.11"338                    ini2toml[lite]>=0.9339                    """,340                "other": [],341            },342        )343        # Generate a PKG-INFO file using setuptools344        return Distribution(attrs)345 346    def test_requires_dist(self, tmp_path):347        dist = self.base_example()348        pkg_info = _get_pkginfo(dist)349        assert _valid_metadata(pkg_info)350 351        # Ensure Requires-Dist is present352        expected = [353            'Metadata-Version:',354            'Requires-Python: >=3.8',355            'Provides-Extra: other',356            'Provides-Extra: testing',357            'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"',358            'Requires-Dist: more-itertools==8.8.0; extra == "other"',359            'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"',360        ]361        for line in expected:362            assert line in pkg_info363 364    HERE = Path(__file__).parent365    EXAMPLES_FILE = HERE / "config/setupcfg_examples.txt"366 367    @pytest.fixture(params=[None, *urls_from_file(EXAMPLES_FILE)])368    def dist(self, request, monkeypatch, tmp_path):369        """Example of distribution with arbitrary configuration"""370        monkeypatch.chdir(tmp_path)371        monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.42"))372        monkeypatch.setattr(expand, "read_files", Mock(return_value="hello world"))373        monkeypatch.setattr(374            Distribution, "_finalize_license_files", Mock(return_value=None)375        )376        if request.param is None:377            yield self.base_example()378        else:379            # Real-world usage380            config = retrieve_file(request.param)381            yield setupcfg.apply_configuration(Distribution({}), config)382 383    @pytest.mark.uses_network384    def test_pkg_info_roundtrip(self, tmp_path, dist):385        """Ensure PKG-INFO round trips according to pypa/wheel's methodology"""386        # Generate an simplified "egg-info" with PKG-INFO387        pkg_info = _get_pkginfo(dist)388 389        # Emulate the way old versions of wheel.bdist_wheel used to parse and regenerate390        # the message, then ensures the metadata generated by setuptools is compatible.391        with io.StringIO(pkg_info) as buffer:392            msg = Parser(EmailMessage).parse(buffer)393 394        serialization_policy = EmailPolicy(395            utf8=True,396            mangle_from_=False,397            max_line_length=0,398        )399        with io.BytesIO() as buffer:400            out = io.TextIOWrapper(buffer, encoding="utf-8")401            Generator(out, policy=serialization_policy).flatten(msg)402            out.flush()403            regenerated = buffer.getvalue()404 405        raw_metadata = bytes(pkg_info, "utf-8")406        # Normalise newlines to avoid test errors on Windows:407        raw_metadata = b"\n".join(raw_metadata.splitlines())408        regenerated = b"\n".join(regenerated.splitlines())409        assert regenerated == raw_metadata410 411 412class TestPEP643:413    STATIC_CONFIG = {414        "setup.cfg": cleandoc(415            """416            [metadata]417            name = package418            version = 0.0.1419            author = Foo Bar420            author_email = foo@bar.net421            long_description = Long422                               description423            description = Short description424            keywords = one, two425            platforms = abcd426            [options]427            install_requires = requests428            """429        ),430        "pyproject.toml": cleandoc(431            """432            [project]433            name = "package"434            version = "0.0.1"435            authors = [436              {name = "Foo Bar", email = "foo@bar.net"}437            ]438            description = "Short description"439            readme = {text = "Long\\ndescription", content-type = "text/plain"}440            keywords = ["one", "two"]441            dependencies = ["requests"]442            license = "AGPL-3.0-or-later"443            [tool.setuptools]444            provides = ["abcd"]445            obsoletes = ["abcd"]446            """447        ),448    }449 450    @pytest.mark.parametrize("file", STATIC_CONFIG.keys())451    def test_static_config_has_no_dynamic(self, file, tmpdir_cwd):452        Path(file).write_text(self.STATIC_CONFIG[file], encoding="utf-8")453        metadata = _get_metadata()454        assert metadata.get_all("Dynamic") is None455        assert metadata.get_all("dynamic") is None456 457    @pytest.mark.parametrize("file", STATIC_CONFIG.keys())458    @pytest.mark.parametrize(459        "fields",460        [461            # Single dynamic field462            {"requires-python": ("python_requires", ">=3.12")},463            {"author-email": ("author_email", "snoopy@peanuts.com")},464            {"keywords": ("keywords", ["hello", "world"])},465            {"platform": ("platforms", ["abcd"])},466            # Multiple dynamic fields467            {468                "summary": ("description", "hello world"),469                "description": ("long_description", "bla bla bla bla"),470                "requires-dist": ("install_requires", ["hello-world"]),471            },472        ],473    )474    def test_modified_fields_marked_as_dynamic(self, file, fields, tmpdir_cwd):475        # We start with a static config476        Path(file).write_text(self.STATIC_CONFIG[file], encoding="utf-8")477        dist = _makedist()478 479        # ... but then we simulate the effects of a plugin modifying the distribution480        for attr, value in fields.values():481            # `dist` and `dist.metadata` are complicated...482            # Some attributes work when set on `dist`, others on `dist.metadata`...483            # Here we set in both just in case (this also avoids calling `_finalize_*`)484            setattr(dist, attr, value)485            setattr(dist.metadata, attr, value)486 487        # Then we should be able to list the modified fields as Dynamic488        metadata = _get_metadata(dist)489        assert set(metadata.get_all("Dynamic")) == set(fields)490 491    @pytest.mark.parametrize(492        "extra_toml",493        [494            "# Let setuptools autofill license-files",495            "license-files = ['LICENSE*', 'AUTHORS*', 'NOTICE']",496        ],497    )498    def test_license_files_dynamic(self, extra_toml, tmpdir_cwd):499        # For simplicity (and for the time being) setuptools is not making500        # any special handling to guarantee `License-File` is considered static.501        # Instead we rely in the fact that, although suboptimal, it is OK to have502        # it as dynamics, as per:503        # https://github.com/pypa/setuptools/issues/4629#issuecomment-2331233677504        files = {505            "pyproject.toml": self.STATIC_CONFIG["pyproject.toml"].replace(506                'license = "AGPL-3.0-or-later"',507                f"dynamic = ['license']\n{extra_toml}",508            ),509            "LICENSE.md": "--- mock license ---",510            "NOTICE": "--- mock notice ---",511            "AUTHORS.txt": "--- me ---",512        }513        # Sanity checks:514        assert extra_toml in files["pyproject.toml"]515        assert 'license = "AGPL-3.0-or-later"' not in extra_toml516 517        jaraco.path.build(files)518        dist = _makedist(license_expression="AGPL-3.0-or-later")519        metadata = _get_metadata(dist)520        assert set(metadata.get_all("Dynamic")) == {521            'license-file',522            'license-expression',523        }524        assert metadata.get("License-Expression") == "AGPL-3.0-or-later"525        assert set(metadata.get_all("License-File")) == {526            "NOTICE",527            "AUTHORS.txt",528            "LICENSE.md",529        }530 531 532def _makedist(**attrs):533    dist = Distribution(attrs)534    dist.parse_config_files()535    return dist536 537 538def _get_pkginfo(dist: Distribution):539    with io.StringIO() as fp:540        dist.metadata.write_pkg_file(fp)541        return fp.getvalue()542 543 544def _get_metadata(dist: Distribution | None = None):545    return message_from_string(_get_pkginfo(dist or _makedist()))546 547 548def _valid_metadata(text: str) -> bool:549    metadata = Metadata.from_email(text, validate=True)  # can raise exceptions550    return metadata is not None551 
codekingpro/portable-devtools · Team Ai