From 7691469d5cf6c62709c2edb8ece5bc9721b404d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Tue, 5 Sep 2023 23:04:51 -0600 Subject: [PATCH] First commit --- .flake8 | 5 + .gitignore | 160 ++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 53 +++++++++++ LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++ README.md | 47 ++++++++++ pyproject.toml | 171 ++++++++++++++++++++++++++++++++++ src/pep610/__init__.py | 150 ++++++++++++++++++++++++++++++ tests/__init__.py | 3 + tests/test_generic.py | 80 ++++++++++++++++ tests/test_parse.py | 95 +++++++++++++++++++ 10 files changed, 965 insertions(+) create mode 100644 .flake8 create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/pep610/__init__.py create mode 100644 tests/__init__.py create mode 100644 tests/test_generic.py create mode 100644 tests/test_parse.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..e62457c --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +select = DAR +docstring_style=google +max-line-length = 88 +strictness = short diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1b33b66 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,53 @@ +ci: + autofix_commit_msg: '[pre-commit.ci] auto fixes' + autofix_prs: true + autoupdate_schedule: monthly + autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-json + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + exclude: \.changes/.*\.md + - id: trailing-whitespace + +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.26.3 + hooks: + - id: check-dependabot + - id: check-github-workflows + - id: check-readthedocs + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.287 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix, --show-fixes] + +- repo: https://github.com/psf/black + rev: 23.7.0 + hooks: + - id: black + language_version: python3.11 + +- repo: https://github.com/pycqa/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + additional_dependencies: + - darglint==1.8.1 + +- repo: https://github.com/pre-commit/pre-commit + rev: v3.4.0 + hooks: + - id: validate_manifest + +- repo: https://github.com/tox-dev/pyproject-fmt + rev: "1.1.0" + hooks: + - id: pyproject-fmt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa7acc7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d66893a --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# pep610 + +[![PyPI - Version](https://img.shields.io/pypi/v/pep610.svg)](https://pypi.org/project/pep610) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pep610.svg)](https://pypi.org/project/pep610) + +----- + +**Table of Contents** + +- [Installation](#installation) +- [Usage](#usage) +- [License](#license) + +## Installation + +```console +pip install pep610 +``` + +## Usage + +```python +from importlib import metadata + +import pep610 + +dist = metadata.distribution('pep610') +data = pep610.read_from_distribution(dist) + +match data: + case pep610.DirData(url, dir_info): + print(f"URL: {url}") + print(f"Editable: {dir_info.editable}") + case pep610.VCSData(url, vcs_info): + print(f"URL: {url}") + print(f"VCS: {vcs_info.vcs}") + print(f"Commit: {vcs_info.commit_id}") + case pep610.ArchiveData(url, archive_info): + print(f"URL: {url}") + print(f"Hash: {archive_info.hash}") + case _: + print("Unknown data") +``` + +## License + +`pep610` is distributed under the terms of the [Apache License 2.0](LICENSE). diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..637113f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,171 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ + "hatch-vcs", + "hatchling", +] + +[project] +name = "pep610" +description = "Python helpers for PEP 610" +readme = "README.md" +keywords = [ +] +license = "Apache-2.0" +authors = [{ name = "Edgar Ramírez Mondragón", email = "edgarrm358@gmail.com" }] +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = [ + "version", +] +dependencies = [ +] +[project.optional-dependencies] +dev = [ + "hypothesis", + "pytest", +] +[project.urls] +Documentation = "https://github.com/unknown/pep610#readme" +Issues = "https://github.com/unknown/pep610/issues" +Source = "https://github.com/unknown/pep610" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.envs.test] +dependencies = [ + "hypothesis", + "hypothesis-jsonschema", + "pytest", +] +[tool.hatch.envs.test.scripts] +run = "pytest {args:tests}" + +[[tool.hatch.envs.test.matrix]] +python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +[tool.hatch.envs.lint] +detached = true +dependencies = [ + "black>=23.1.0", + "mypy>=1.0.0", + "hypothesis", + "hypothesis-jsonschema", + "pytest", + "ruff>=0.0.243", +] +[tool.hatch.envs.lint.scripts] +typing = "mypy --install-types --non-interactive {args:src/pep610 tests}" +style = ["ruff {args:.}", "black --check --diff {args:.}"] +fmt = ["black {args:.}", "ruff --fix {args:.}", "style"] +all = ["style", "typing"] + +[tool.black] +target-version = ["py38"] +line-length = 100 +skip-string-normalization = true + +[tool.ruff] +target-version = "py38" +line-length = 100 +select = [ + "F", + "E", + "W", + "C90", + "I", + "N", + "D", + "UP", + "YTT", + "ANN", + "S", + "BLE", + "FBT", + "B", + "A", + "COM", + "C4", + "DTZ", + "T10", + "EM", + "FA", + "ISC", + "ICN", + "G", + "INP", + "PIE", + "T20", + "PT", + "Q", + "RSE", + "RET", + "SLF", + "SIM", + "TID", + "TCH", + "INT", + "ARG", + "PTH", + "TD", + "FIX", + "ERA", + "PGH", + "PLC", + "PLE", + "PLR", + "PLW", + "TRY", + "FLY", + "PERF", + "FURB", + "RUF", +] +unfixable = [ + "ERA", # commented-out-code +] + +[tool.ruff.isort] +known-first-party = ["pep610"] + +[tool.ruff.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.per-file-ignores] +"tests/**/*" = [ + "PLR2004", # magic-value-comparison + "S101", # assert + "TID252", # relative-imports + "D100", # undocumented-public-module + "D104", # undocumented-public-package + "ANN201", # missing-return-type-undocumented-public-function +] + +[tool.ruff.pydocstyle] +convention = "google" + +[tool.pytest.ini_options] +addopts = "-v" + +[tool.coverage.run] +source_pkgs = ["pep610", "tests"] +branch = true +parallel = true + +[tool.coverage.paths] +pep610 = ["src/pep610", "*/pep610/src/pep610"] +tests = ["tests", "*/pep610/tests"] + +[tool.coverage.report] +exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] diff --git a/src/pep610/__init__.py b/src/pep610/__init__.py new file mode 100644 index 0000000..6ac0026 --- /dev/null +++ b/src/pep610/__init__.py @@ -0,0 +1,150 @@ +"""PEP 610 parser.""" + +from __future__ import annotations + +import json +import typing as t +from dataclasses import dataclass +from importlib.metadata import version +from pathlib import Path + +if t.TYPE_CHECKING: + from importlib.metadata import Distribution + from os import PathLike + +__version__ = version(__package__) + + +class PEP610Error(Exception): + """Base exception for PEP 610 errors.""" + + +@dataclass +class VCSInfo: + """VCS information.""" + + vcs: str + commit_id: str + requested_revision: str | None = None + resolved_revision: str | None = None + resolved_revision_type: str | None = None + + +@dataclass +class VCSData: + """VCS direct URL data.""" + + url: str + vcs_info: VCSInfo + + +class HashData(t.NamedTuple): + """Archive hash data.""" + + algorithm: str + value: str + + +@dataclass +class ArchiveInfo: + """Archive information.""" + + hash: HashData | None # noqa: A003 + + +@dataclass +class ArchiveData: + """Archive direct URL data.""" + + url: str + archive_info: ArchiveInfo + + +@dataclass +class DirInfo: + """Local directory information.""" + + editable: bool + + +@dataclass +class DirData: + """Local directory direct URL data.""" + + url: str + dir_info: DirInfo + + +def parse(path: PathLike[str]) -> VCSData | ArchiveData | DirData: + """Parse a PEP 610 file. + + Args: + path: The path to the PEP 610 file. + + Returns: + The parsed PEP 610 file. + + Raises: + PEP610Error: If the PEP 610 file is invalid. + """ + with Path(path).open() as f: + try: + result = _parse(f.read()) + except json.JSONDecodeError as e: + msg = f"Failed to parse {path}" + raise PEP610Error(msg) from e + + if result is None: + errmsg = f"Unknown PEP 610 file format: {path}" + raise PEP610Error(errmsg) + + return result + + +def _parse(content: str) -> VCSData | ArchiveData | DirData | None: + data = json.loads(content) + + if "archive_info" in data: + hash_value = data["archive_info"].get("hash") + hash_data = HashData(*hash_value.split("=", 1)) if hash_value else None + return ArchiveData( + url=data["url"], + archive_info=ArchiveInfo(hash=hash_data), + ) + + if "dir_info" in data: + return DirData( + url=data["url"], + dir_info=DirInfo( + editable=data["dir_info"].get("editable", False), + ), + ) + + if "vcs_info" in data: + return VCSData( + url=data["url"], + vcs_info=VCSInfo( + vcs=data["vcs_info"]["vcs"], + commit_id=data["vcs_info"]["commit_id"], + requested_revision=data["vcs_info"].get("requested_revision"), + resolved_revision=data["vcs_info"].get("resolved_revision"), + resolved_revision_type=data["vcs_info"].get("resolved_revision_type"), + ), + ) + + return None + + +def read_from_distribution(dist: Distribution) -> VCSData | ArchiveData | DirData | None: + """Read the package data for a given package. + + Args: + dist: The package distribution. + + Returns: + The parsed PEP 610 file. + """ + if contents := dist.read_text("direct_url.json"): + return _parse(contents) + + return None diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..54e3a36 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2023-present Edgar Ramírez Mondragón +# +# SPDX-License-Identifier: MIT diff --git a/tests/test_generic.py b/tests/test_generic.py new file mode 100644 index 0000000..dac9520 --- /dev/null +++ b/tests/test_generic.py @@ -0,0 +1,80 @@ +import json + +import pytest +from hypothesis import given +from hypothesis_jsonschema import from_schema + +from pep610 import parse + + +@given( + from_schema( + { + "allOf": [ + { + "type": "object", + "properties": { + "url": {"type": "string", "format": "uri"}, + }, + "required": ["url"], + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "dir_info": { + "type": "object", + "properties": {"editable": {"type": "boolean"}}, + }, + }, + "required": ["dir_info"], + }, + { + "type": "object", + "properties": { + "vcs_info": { + "type": "object", + "properties": { + "vcs": { + "type": "string", + "enum": ["git", "hg", "bzr", "svn"], + }, + "requested_revision": {"type": "string"}, + "commit_id": {"type": "string"}, + "resolved_revision": {"type": "string"}, + "resolved_revision_type": {"type": "string"}, + }, + "required": ["vcs", "commit_id"], + }, + }, + "required": ["vcs_info"], + }, + { + "type": "object", + "properties": { + "archive_info": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "pattern": r"^[a-f0-9]+=[a-f0-9]+$", + }, + }, + }, + }, + "required": ["archive_info"], + }, + ], + }, + ], + }, + ), +) +def test_generic(tmp_path_factory: pytest.TempPathFactory, value: dict): + """Test parsing a local directory.""" + filepath = tmp_path_factory.mktemp("pep610").joinpath("direct_url.json") + with filepath.open("w") as f: + f.write(json.dumps(value)) + + parse(filepath) diff --git a/tests/test_parse.py b/tests/test_parse.py new file mode 100644 index 0000000..a8b478e --- /dev/null +++ b/tests/test_parse.py @@ -0,0 +1,95 @@ +"""Test the PEP 610 parser.""" + +import json +from pathlib import Path + +import pytest + +from pep610 import ArchiveData, ArchiveInfo, DirData, DirInfo, HashData, VCSData, VCSInfo, parse + + +@pytest.mark.parametrize( + ("data", "expected"), + [ + pytest.param( + {"url": "file:///home/user/project", "dir_info": {"editable": True}}, + DirData( + url="file:///home/user/project", + dir_info=DirInfo(editable=True), + ), + id="local_editable", + ), + pytest.param( + {"url": "file:///home/user/project", "dir_info": {"editable": False}}, + DirData( + url="file:///home/user/project", + dir_info=DirInfo(editable=False), + ), + id="local_not_editable", + ), + pytest.param( + {"url": "file:///home/user/project", "dir_info": {}}, + DirData( + url="file:///home/user/project", + dir_info=DirInfo(editable=False), + ), + id="local_no_editable_info", + ), + pytest.param( + { + "url": "https://github.com/pypa/pip/archive/1.3.1.zip", + "archive_info": { + "hash": "sha256=2dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db8", # noqa: E501 + }, + }, + ArchiveData( + url="https://github.com/pypa/pip/archive/1.3.1.zip", + archive_info=ArchiveInfo( + hash=HashData( + "sha256", + "2dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db8", + ), + ), + ), + id="archive_sha256", + ), + pytest.param( + { + "url": "file://path/to/my.whl", + "archive_info": {}, + }, + ArchiveData( + url="file://path/to/my.whl", + archive_info=ArchiveInfo(hash=None), + ), + id="archive_no_hash", + ), + pytest.param( + { + "url": "https://github.com/pypa/pip.git", + "vcs_info": { + "vcs": "git", + "requested_revision": "1.3.1", + "resolved_revision_type": "tag", + "commit_id": "7921be1537eac1e97bc40179a57f0349c2aee67d", + }, + }, + VCSData( + url="https://github.com/pypa/pip.git", + vcs_info=VCSInfo( + vcs="git", + requested_revision="1.3.1", + resolved_revision_type="tag", + commit_id="7921be1537eac1e97bc40179a57f0349c2aee67d", + ), + ), + id="vcs_git", + ), + ], +) +def test_parse(data: dict, expected: object, tmp_path: Path): + """Test the parse function.""" + filepath = tmp_path.joinpath("direct_url.json") + with filepath.open("w") as f: + json.dump(data, f) + assert parse(filepath) == expected