diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcbd72719d..2f113788b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: branches: - main - dev - - '1.x' + - "1.x" paths-ignore: - "docs/**" - "news/**" @@ -51,11 +51,6 @@ jobs: - uses: actions/checkout@v2 with: submodules: true - - name: Set Python 2.7 - uses: actions/setup-python@v2 - with: - python-version: 2.7 - architecture: ${{ matrix.arch }} - name: Set Python 3.6 uses: actions/setup-python@v2 with: @@ -79,6 +74,12 @@ jobs: with: python-version: 3.9 architecture: ${{ matrix.arch }} + - name: Set Python 3.10 + uses: actions/setup-python@v2 + if: matrix.python-version != '3.10' + with: + python-version: "3.10" + architecture: ${{ matrix.arch }} - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: diff --git a/docs/docs/usage/project.md b/docs/docs/usage/project.md index 373ceb57a0..19927ba052 100644 --- a/docs/docs/usage/project.md +++ b/docs/docs/usage/project.md @@ -215,10 +215,13 @@ PDM provides `import` command so that you don't have to initialize the project m 1. Pipenv's `Pipfile` 2. Poetry's section in `pyproject.toml` 3. Flit's section in `pyproject.toml` -4. `requirements.txt` format used by Pip +4. `requirements.txt` format used by pip +5. setuptools `setup.py` -Also, when you are executing `pdm init` or `pdm install`, PDM can auto-detect possible files to import -if your PDM project has not been initialized yet. +Also, when you are executing `pdm init` or `pdm install`, PDM can auto-detect possible files to import if your PDM project has not been initialized yet. + +!!! attention "CAUTION" + Converting a `setup.py` will execute the file with the project interpreter. Make sure `setuptools` is installed with the interpreter and the `setup.py` is trusted. ## Export locked packages to alternative formats diff --git a/news/1062.feature.md b/news/1062.feature.md new file mode 100644 index 0000000000..48fdfef017 --- /dev/null +++ b/news/1062.feature.md @@ -0,0 +1 @@ +Add support for importing from a `setup.py` project. diff --git a/pdm.lock b/pdm.lock index 5c4d5d1be0..9115ee949b 100644 --- a/pdm.lock +++ b/pdm.lock @@ -265,7 +265,7 @@ version = "8.2.13" requires_python = ">=3.7" summary = "Documentation that simply works" dependencies = [ - "jinja2>=3.0.2", + "jinja2>=2.11.1", "markdown>=3.2", "mkdocs-material-extensions>=1.0.3", "mkdocs>=1.3.0", @@ -620,6 +620,11 @@ version = "1.26.9" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" summary = "HTTP library with thread-safe connection pooling, file post, and more." +[[package]] +name = "verspec" +version = "0.1.0" +summary = "Flexible version handling" + [[package]] name = "watchdog" version = "2.1.7" @@ -640,7 +645,7 @@ summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "3.1" -content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49a663e86" +content_hash = "sha256:ba404c54dee3fe0aa4fc110749724b2fbb86806b89e7b9d526357fb8293266e1" [metadata.files] "arpeggio 1.10.2" = [ @@ -694,7 +699,6 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] "coverage 6.3.2" = [ - {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, @@ -733,6 +737,7 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, ] @@ -788,7 +793,6 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "markdown-include-0.6.0.tar.gz", hash = "sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"}, ] "markupsafe 2.1.1" = [ - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, @@ -827,6 +831,7 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] "mergedeep 1.3.4" = [ @@ -866,7 +871,6 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "mkdocstrings-python-legacy-0.2.2.tar.gz", hash = "sha256:f0e7ec6a19750581b752acb38f6b32fcd1efe006f14f6703125d2c2c9a5c6f02"}, ] "msgpack 1.0.3" = [ - {file = "msgpack-1.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:f01b26c2290cbd74316990ba84a14ac3d599af9cebefc543d241a66e785cf17d"}, {file = "msgpack-1.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96acc674bb9c9be63fa8b6dabc3248fdc575c4adc005c440ad02f87ca7edd079"}, {file = "msgpack-1.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c3ca57c96c8e69c1a0d2926a6acf2d9a522b41dc4253a8945c4c6cd4981a4e3"}, {file = "msgpack-1.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0a792c091bac433dfe0a70ac17fc2087d4595ab835b47b89defc8bbabcf5c73"}, @@ -899,6 +903,7 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "msgpack-1.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a598d0685e4ae07a0672b59792d2cc767d09d7a7f39fd9bd37ff84e060b1a996"}, {file = "msgpack-1.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4c309a68cb5d6bbd0c50d5c71a25ae81f268c2dc675c6f4ea8ab2feec2ac4e2"}, {file = "msgpack-1.0.3-cp39-cp39-win32.whl", hash = "sha256:494471d65b25a8751d19c83f1a482fd411d7ca7a3b9e17d25980a74075ba0e88"}, + {file = "msgpack-1.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:f01b26c2290cbd74316990ba84a14ac3d599af9cebefc543d241a66e785cf17d"}, {file = "msgpack-1.0.3.tar.gz", hash = "sha256:51fdc7fb93615286428ee7758cecc2f374d5ff363bdd884c7ea622a7a327a81e"}, ] "packaging 21.3" = [ @@ -982,7 +987,6 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "pytkdocs-0.16.1.tar.gz", hash = "sha256:e2ccf6dfe9dbbceb09818673f040f1a7c32ed0bffb2d709b06be6453c4026045"}, ] "pyyaml 6.0" = [ - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -1014,6 +1018,7 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] "pyyaml-env-tag 0.1" = [ @@ -1072,8 +1077,11 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] +"verspec 0.1.0" = [ + {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, + {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, +] "watchdog 2.1.7" = [ - {file = "watchdog-2.1.7-py3-none-win_amd64.whl", hash = "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566"}, {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383"}, {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4"}, {file = "watchdog-2.1.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca"}, @@ -1095,6 +1103,7 @@ content_hash = "sha256:81d85f3e6b79cd961a23c768dbe8c3307c7c7a484ad14db7357ec0d49 {file = "watchdog-2.1.7-py3-none-manylinux2014_s390x.whl", hash = "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385"}, {file = "watchdog-2.1.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055"}, {file = "watchdog-2.1.7-py3-none-win32.whl", hash = "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6"}, + {file = "watchdog-2.1.7-py3-none-win_amd64.whl", hash = "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566"}, {file = "watchdog-2.1.7-py3-none-win_ia64.whl", hash = "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572"}, {file = "watchdog-2.1.7.tar.gz", hash = "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480"}, ] diff --git a/pdm/cli/utils.py b/pdm/cli/utils.py index 0838742bad..75ded92914 100644 --- a/pdm/cli/utils.py +++ b/pdm/cli/utils.py @@ -500,6 +500,7 @@ def find_importable_files(project: Project) -> Iterable[tuple[str, Path]]: "pyproject.toml", "requirements.in", "requirements.txt", + "setup.py", ): project_file = project.root / filename if not project_file.exists(): diff --git a/pdm/formats/setup_py.py b/pdm/formats/setup_py.py index cc0adc1b50..068471a809 100644 --- a/pdm/formats/setup_py.py +++ b/pdm/formats/setup_py.py @@ -1,7 +1,8 @@ import os from pathlib import Path -from typing import Any, List, Optional +from typing import Any, Dict, List, Mapping, Optional, Tuple +from pdm.formats.base import array_of_inline_tables, make_array, make_inline_table from pdm.project import Project @@ -9,8 +10,54 @@ def check_fingerprint(project: Project, filename: Path) -> bool: return os.path.basename(filename) == "setup.py" -def convert(project: Project, filename: Path, options: Optional[Any]) -> None: - raise NotImplementedError() +def convert( + project: Project, filename: Path, options: Optional[Any] +) -> Tuple[Mapping[str, Any], Mapping[str, Any]]: + from pdm.models.in_process import parse_setup_py + + parsed = parse_setup_py( + str(project.environment.interpreter.executable), str(filename) + ) + metadata: Dict[str, Any] = {} + settings: Dict[str, Any] = {} + for name in [ + "name", + "version", + "description", + "keywords", + "urls", + "readme", + ]: + if name in parsed: + metadata[name] = parsed[name] + if "authors" in parsed: + metadata["authors"] = array_of_inline_tables(parsed["authors"]) + if "maintainers" in parsed: + metadata["maintainers"] = array_of_inline_tables(parsed["maintainers"]) + if "classifiers" in parsed: + metadata["classifiers"] = make_array(sorted(parsed["classifiers"]), True) + if "python_requires" in parsed: + metadata["requires-python"] = parsed["python_requires"] + if "install_requires" in parsed: + metadata["dependencies"] = make_array(sorted(parsed["install_requires"]), True) + if "extras_require" in parsed: + metadata["optional-dependencies"] = { + k: make_array(sorted(v), True) for k, v in parsed["extras_require"].items() + } + if "license" in parsed: + metadata["license"] = make_inline_table({"text": parsed["license"]}) + if "package_dir" in parsed: + settings["package-dir"] = parsed["package_dir"] + + entry_points = parsed.get("entry_points", {}) + if "console_scripts" in entry_points: + metadata["scripts"] = entry_points.pop("console_scripts") + if "gui_scripts" in entry_points: + metadata["gui-scripts"] = entry_points.pop("gui_scripts") + if entry_points: + metadata["entry-points"] = entry_points + + return metadata, settings def export(project: Project, candidates: List, options: Optional[Any]) -> str: diff --git a/pdm/models/in_process/__init__.py b/pdm/models/in_process/__init__.py index bd8b57480e..2687940a48 100644 --- a/pdm/models/in_process/__init__.py +++ b/pdm/models/in_process/__init__.py @@ -6,7 +6,7 @@ import os import subprocess from pathlib import Path -from typing import Dict, Optional +from typing import Any, Dict, Optional FOLDER_PATH = Path(__file__).parent @@ -35,3 +35,9 @@ def get_pep508_environment(executable: str) -> Dict[str, str]: script = str(FOLDER_PATH / "pep508.py") args = [executable, "-Es", script] return json.loads(subprocess.check_output(args)) + + +def parse_setup_py(executable: str, path: str) -> Dict[str, Any]: + """Parse setup.py and return the kwargs""" + cmd = [executable, "-Es", str(FOLDER_PATH / "parse_setup.py"), path] + return json.loads(subprocess.check_output(cmd)) diff --git a/pdm/models/in_process/parse_setup.py b/pdm/models/in_process/parse_setup.py new file mode 100644 index 0000000000..a36d0c206f --- /dev/null +++ b/pdm/models/in_process/parse_setup.py @@ -0,0 +1,211 @@ +import os +import sys +from typing import Any, Dict + + +def _parse_setup_cfg(path: str) -> Dict[str, Any]: + import configparser + + setup_cfg = configparser.ConfigParser() + setup_cfg.read(path, encoding="utf-8") + + result: Dict[str, Any] = {} + if not setup_cfg.has_section("metadata"): + return result + + metadata = setup_cfg["metadata"] + + if "name" in metadata: + result["name"] = metadata["name"] + + if "description" in metadata: + result["description"] = metadata["description"] + + if "license" in metadata: + result["license"] = metadata["license"] + + if "author" in metadata: + result["author"] = metadata["author"] + + if "author_email" in metadata: + result["author_email"] = metadata["author_email"] + + if "maintainer" in metadata: + result["maintainer"] = metadata["maintainer"] + + if "maintainer_email" in metadata: + result["maintainer_email"] = metadata["maintainer_email"] + + if "keywords" in metadata: + keywords = metadata["keywords"].strip().splitlines() + result["keywords"] = keywords if len(keywords) > 1 else keywords[0] + + if "classifiers" in metadata: + result["classifiers"] = metadata["classifiers"].strip().splitlines() + + if "url" in metadata: + result["url"] = metadata["url"] + + if "download_url" in metadata: + result["download_url"] = metadata["download_url"] + + if "project_urls" in metadata: + result["project_urls"] = dict( + [u.strip() for u in url.split("=", 1)] + for url in metadata["project_urls"].strip().splitlines() + ) + + if "long_description" in metadata: + long_description = metadata["long_description"].strip() + if long_description.startswith("file:"): + result["readme"] = long_description[5:].strip() + + if setup_cfg.has_section("options"): + options = setup_cfg["options"] + + if "python_requires" in options: + result["python_requires"] = options["python_requires"] + + if "install_requires" in options: + result["install_requires"] = ( + options["install_requires"].strip().splitlines() + ) + + if "package_dir" in options: + result["package_dir"] = dict( + [p.strip() for p in d.split("=", 1)] + for d in options["package_dir"].strip().splitlines() + ) + + if setup_cfg.has_section("options.extras_require"): + result["extras_require"] = { + feature: dependencies.strip().splitlines() + for feature, dependencies in setup_cfg["options.extras_require"].items() + } + + if setup_cfg.has_section("options.entry_points"): + result["entry_points"] = { + entry_point: definitions.strip().splitlines() + for entry_point, definitions in setup_cfg["options.entry_points"].items() + } + + return result + + +setup_kwargs = {} +SUPPORTED_ARGS = ( + "name", + "version", + "description", + "license", + "author", + "author_email", + "maintainer", + "maintainer_email", + "keywords", + "classifiers", + "url", + "download_url", + "project_urls", + "python_requires", + "install_requires", + "extras_require", + "entry_points", + "package_dir", +) + + +def fake_setup(**kwargs): + setup_kwargs.update((k, v) for k, v in kwargs.items() if k in SUPPORTED_ARGS) + + +def clean_metadata(metadata: Dict[str, Any]) -> None: + author = {} + if "author" in metadata: + author["name"] = metadata.pop("author") + if "author_email" in metadata: + author["email"] = metadata.pop("author_email") + if author: + metadata["authors"] = [author] + maintainer = {} + if "maintainer" in metadata: + maintainer["name"] = metadata.pop("maintainer") + if "maintainer_email" in metadata: + maintainer["email"] = metadata.pop("maintainer_email") + if maintainer: + metadata["maintainers"] = [maintainer] + + urls = {} + if "url" in metadata: + urls["Homepage"] = metadata.pop("url") + if "download_url" in metadata: + urls["Downloads"] = metadata.pop("download_url") + if "project_urls" in metadata: + urls.update(metadata.pop("project_urls")) + if urls: + metadata["urls"] = urls + + if "" in metadata.get("package_dir", {}): + metadata["package_dir"] = metadata["package_dir"][""] + + if "keywords" in metadata: + keywords = metadata["keywords"] + if isinstance(keywords, str): + keywords = [k.strip() for k in keywords.split(",")] + metadata["keywords"] = keywords + + if "entry_points" in metadata and isinstance(metadata["entry_points"], dict): + entry_points = {} + for entry_point, definitions in metadata["entry_points"].items(): + if isinstance(definitions, str): + definitions = [definitions] + definitions = dict( + sorted(d.replace(" ", "").split("=", 1) for d in definitions) + ) + + entry_points[entry_point] = definitions + if entry_points: + metadata["entry_points"] = dict(sorted(entry_points.items())) + + +def parse_setup(path: str) -> Dict[str, Any]: + import tokenize + + import setuptools + + setuptools.setup = fake_setup + + parsed: Dict[str, Any] = {} + path = os.path.abspath(path) + project_path = os.path.dirname(path) + os.chdir(project_path) + if os.path.exists("setup.cfg"): + parsed.update(_parse_setup_cfg("setup.cfg")) + + # Execute setup.py and get the kwargs + __file__ = sys.argv[0] = path + sys.path.insert(0, project_path) + setup_kwargs.clear() + + with tokenize.open(path) as f: + code = f.read() + exec( + compile(code, __file__, "exec"), + {"__name__": "__main__", "__file__": __file__, "setup_kwargs": setup_kwargs}, + ) + parsed.update(setup_kwargs) + + if "readme" not in parsed: + for readme_file in ("README.md", "README.rst", "README.txt"): + readme_path = os.path.join(project_path, readme_file) + if os.path.exists(readme_path): + parsed["readme"] = readme_file + break + clean_metadata(parsed) + return parsed + + +if __name__ == "__main__": + import json + + print(json.dumps(parse_setup(sys.argv[1]))) diff --git a/tests/fixtures/projects/test-setuptools/AUTHORS b/tests/fixtures/projects/test-setuptools/AUTHORS new file mode 100644 index 0000000000..900c418024 --- /dev/null +++ b/tests/fixtures/projects/test-setuptools/AUTHORS @@ -0,0 +1 @@ +frostming diff --git a/tests/fixtures/projects/test-setuptools/README.md b/tests/fixtures/projects/test-setuptools/README.md new file mode 100644 index 0000000000..005ce84941 --- /dev/null +++ b/tests/fixtures/projects/test-setuptools/README.md @@ -0,0 +1 @@ +# My Module diff --git a/tests/fixtures/projects/test-setuptools/mymodule.py b/tests/fixtures/projects/test-setuptools/mymodule.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/tests/fixtures/projects/test-setuptools/mymodule.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/tests/fixtures/projects/test-setuptools/setup.cfg b/tests/fixtures/projects/test-setuptools/setup.cfg new file mode 100644 index 0000000000..8275a81b4b --- /dev/null +++ b/tests/fixtures/projects/test-setuptools/setup.cfg @@ -0,0 +1,20 @@ +[metadata] +name = mymodule +description = A test module +keywords = one, two +classifiers = + Framework :: Django + Programming Language :: Python :: 3 + +[options] +zip_safe = False +include_package_data = True +python_requires = >=3.5 +package_dir = = src +install_requires = + requests + importlib-metadata; python_version<"3.8" + +[options.entry_points] +console_scripts = + mycli = mymodule:main diff --git a/tests/fixtures/projects/test-setuptools/setup.py b/tests/fixtures/projects/test-setuptools/setup.py new file mode 100644 index 0000000000..58d6436d46 --- /dev/null +++ b/tests/fixtures/projects/test-setuptools/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup +from mymodule import __version__ + +with open("AUTHORS", "r") as f: + authors = f.read().strip() + +kwargs = { + "name": "mymodule", + "version": __version__, + "author": authors, +} + +if 1 + 1 >= 2: + kwargs.update(license="MIT") + + +if __name__ == "__main__": + setup(**kwargs) diff --git a/tests/test_formats.py b/tests/test_formats.py index 957f128662..963de253df 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -181,3 +181,23 @@ def test_export_replace_project_root(project): req = parse_requirement(f"./{artifact.name}") result = requirements.export(project, [req], Namespace(hashes=False)) assert "${PROJECT_ROOT}" not in result + + +def test_convert_setup_py_project(project): + golden_file = FIXTURES / "projects/test-setuptools/setup.py" + assert setup_py.check_fingerprint(project, golden_file) + result, settings = setup_py.convert(project, golden_file, Namespace()) + assert result == { + "name": "mymodule", + "version": "0.1.0", + "description": "A test module", + "keywords": ["one", "two"], + "readme": "README.md", + "authors": [{"name": "frostming"}], + "license": {"text": "MIT"}, + "classifiers": ["Framework :: Django", "Programming Language :: Python :: 3"], + "requires-python": ">=3.5", + "dependencies": ['importlib-metadata; python_version<"3.8"', "requests"], + "scripts": {"mycli": "mymodule:main"}, + } + assert settings == {"package-dir": "src"} diff --git a/tests/test_integration.py b/tests/test_integration.py index 25dbe5fee5..2667a7c2fd 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -5,7 +5,7 @@ @pytest.mark.integration @pytest.mark.network -@pytest.mark.parametrize("python_version", ["2.7", "3.6", "3.7", "3.8", "3.9"]) +@pytest.mark.parametrize("python_version", ["3.6", "3.7", "3.8", "3.9", "3.10"]) def test_basic_integration(python_version, core, tmp_path, invoke): """An e2e test case to ensure PDM works on all supported Python versions""" project = core.create_project(tmp_path)