diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 2b055f2389..0000000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[bumpversion] -current_version = 72.1.0 -commit = True -tag = True - -[bumpversion:file:pyproject.toml] diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 092612cb21..0000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,20 +0,0 @@ -recursive-include setuptools *.py *.exe *.xml *.tmpl -recursive-include tests *.py -recursive-include setuptools/tests *.html -recursive-include docs *.py *.txt *.rst *.conf *.css *.css_t Makefile indexsidebar.html -recursive-include setuptools/_vendor * -recursive-include pkg_resources *.py *.txt -recursive-include pkg_resources/tests/data * -recursive-include tools * -recursive-include newsfragments * -include *.py -include *.rst -include MANIFEST.in -include LICENSE -include launcher.c -include msvc-build-launcher.cmd -include mypy.ini -include pytest.ini -include tox.ini -include setuptools/tests/config/setupcfg_examples.txt -global-exclude *.py[cod] __pycache__ diff --git a/conftest.py b/conftest.py index 532e83112a..faff81fce0 100644 --- a/conftest.py +++ b/conftest.py @@ -33,6 +33,7 @@ def pytest_configure(config): 'setuptools/_distutils', '_distutils_hack', 'pkg_resources/tests/data', + 'setuptools/_build_vendor', 'setuptools/_vendor', 'setuptools/config/_validate_pyproject', 'setuptools/modified.py', diff --git a/newsfragments/4530.feature.rst b/newsfragments/4530.feature.rst new file mode 100644 index 0000000000..a13f80bcbf --- /dev/null +++ b/newsfragments/4530.feature.rst @@ -0,0 +1 @@ +Replace static version with setuptools-scm version. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0c7d5e752a..57a9152544 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,6 @@ backend-path = ["."] [project] name = "setuptools" -version = "72.1.0" authors = [ { name = "Python Packaging Authority", email = "distutils-sig@python.org" }, ] @@ -26,6 +25,7 @@ keywords = ["CPAN PyPI distutils eggs package management"] requires-python = ">=3.8" dependencies = [ ] +dynamic = ["version"] [project.urls] Source = "https://github.com/pypa/setuptools" @@ -122,6 +122,8 @@ core = [ "platformdirs >= 2.6.2", ] +[tool.setuptools_scm] + [project.entry-points."distutils.commands"] alias = "setuptools.command.alias:alias" bdist_egg = "setuptools.command.bdist_egg:bdist_egg" @@ -192,5 +194,3 @@ namespaces = true [tool.distutils.sdist] formats = "zip" - -[tool.setuptools_scm] diff --git a/setup.py b/setup.py index 542edaea68..de433dc0d5 100755 --- a/setup.py +++ b/setup.py @@ -9,6 +9,7 @@ here = os.path.dirname(__file__) +sys.path.extend(((vendor_path := os.path.join(os.path.dirname(__file__), 'setuptools', '_build_vendor')) not in sys.path) * [vendor_path]) # fmt: skip package_data = dict( setuptools=['script (dev).tmpl', 'script.tmpl', 'site-patch.py'], diff --git a/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/INSTALLER b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/INSTALLER new file mode 100644 index 0000000000..a1b589e38a --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/LICENSE b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/LICENSE new file mode 100644 index 0000000000..89de354795 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/METADATA b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/METADATA new file mode 100644 index 0000000000..edf8a1d4a1 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/METADATA @@ -0,0 +1,166 @@ +Metadata-Version: 2.1 +Name: setuptools-scm +Version: 8.1.0 +Summary: the blessed package to manage your versions by scm tags +Author-email: Ronny Pfannschmidt +License: Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +Project-URL: documentation, https://setuptools-scm.readthedocs.io/ +Project-URL: repository, https://github.com/pypa/setuptools_scm/ +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Software Development :: Version Control +Classifier: Topic :: System :: Software Distribution +Classifier: Topic :: Utilities +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: packaging >=20 +Requires-Dist: setuptools +Requires-Dist: typing-extensions ; python_version < "3.10" +Requires-Dist: tomli >=1 ; python_version < "3.11" +Provides-Extra: docs +Requires-Dist: entangled-cli ~=2.0 ; extra == 'docs' +Requires-Dist: mkdocs ; extra == 'docs' +Requires-Dist: mkdocs-entangled-plugin ; extra == 'docs' +Requires-Dist: mkdocs-material ; extra == 'docs' +Requires-Dist: mkdocstrings[python] ; extra == 'docs' +Requires-Dist: pygments ; extra == 'docs' +Provides-Extra: rich +Requires-Dist: rich ; extra == 'rich' +Provides-Extra: test +Requires-Dist: build ; extra == 'test' +Requires-Dist: pytest ; extra == 'test' +Requires-Dist: rich ; extra == 'test' +Requires-Dist: wheel ; extra == 'test' +Requires-Dist: typing-extensions ; (python_version < "3.11") and extra == 'test' +Provides-Extra: toml + +# setuptools_scm +[![github ci](https://github.com/pypa/setuptools_scm/workflows/python%20tests+artifacts+release/badge.svg)](https://github.com/pypa/setuptools_scm/actions) +[![Documentation Status](https://readthedocs.org/projects/setuptools-scm/badge/?version=latest)](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest) +[![tidelift](https://tidelift.com/badges/package/pypi/setuptools-scm) ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme) + +## about + +[setuptools-scm] extracts Python package versions from `git` or +`hg` metadata instead of declaring them as the version argument +or in an SCM managed file. + +Additionally, [setuptools-scm] provides setuptools +with a list of files that are managed by the SCM
+(i.e. it automatically adds **all of** the SCM-managed files to the sdist).
+Unwanted files must be excluded via `MANIFEST.in`. + + +## `pyproject.toml` usage + +The preferred way to configure [setuptools-scm] is to author +settings in a `tool.setuptools_scm` section of `pyproject.toml`. + +This feature requires setuptools 61 or later. +First, ensure that [setuptools-scm] is present during the project's +build step by specifying it as one of the build requirements. + +```toml title="pyproject.toml" +[build-system] +requires = ["setuptools>=64", "setuptools_scm>=8"] +build-backend = "setuptools.build_meta" +``` + +That will be sufficient to require [setuptools-scm] for projects +that support [PEP 518] like [pip] and [build]. + +[pip]: https://pypi.org/project/pip +[build]: https://pypi.org/project/build +[PEP 518]: https://peps.python.org/pep-0518/ + + +To enable version inference, you need to set the version +dynamically in the `project` section of `pyproject.toml`: + +```toml title="pyproject.toml" +[project] +# version = "0.0.1" # Remove any existing version parameter. +dynamic = ["version"] + +[tool.setuptools_scm] +``` + +Additionally, a version file can be written by specifying: + +```toml title="pyproject.toml" +[tool.setuptools_scm] +version_file = "pkg/_version.py" +``` + +Where `pkg` is the name of your package. + +If you need to confirm which version string is being generated or debug the configuration, +you can install [setuptools-scm] directly in your working environment and run: + +```console +$ python -m setuptools_scm +# To explore other options, try: +$ python -m setuptools_scm --help +``` + +For further configuration see the [documentation]. + +[setuptools-scm]: https://github.com/pypa/setuptools_scm +[documentation]: https://setuptools-scm.readthedocs.io/ + + +## Interaction with Enterprise Distributions + +Some enterprise distributions like RHEL7 +ship rather old setuptools versions. + +In those cases its typically possible to build by using an sdist against `setuptools_scm<2.0`. +As those old setuptools versions lack sensible types for versions, +modern [setuptools-scm] is unable to support them sensibly. + +It's strongly recommended to build a wheel artifact using modern Python and setuptools, +then installing the artifact instead of trying to run against old setuptools versions. + + +## Code of Conduct + + +Everyone interacting in the [setuptools-scm] project's codebases, issue +trackers, chat rooms, and mailing lists is expected to follow the +[PSF Code of Conduct]. + +[PSF Code of Conduct]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + + +## Security Contact + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/RECORD b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/RECORD new file mode 100644 index 0000000000..b0b4a5c234 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/RECORD @@ -0,0 +1,67 @@ +setuptools_scm-8.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools_scm-8.1.0.dist-info/LICENSE,sha256=iYB6zyMJvShfAzQE7nhYFgLzzZuBmhasLw5fYP9KRz4,1023 +setuptools_scm-8.1.0.dist-info/METADATA,sha256=JstkuN1RJAjyijO1FYrd2ZfrphMgbl-j1LyiGmVbzrQ,6614 +setuptools_scm-8.1.0.dist-info/RECORD,, +setuptools_scm-8.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools_scm-8.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +setuptools_scm-8.1.0.dist-info/entry_points.txt,sha256=7VjBrJmw12qwaWG0yLbRs5fYIf7g4ySdOwHNsJMhy4A,1776 +setuptools_scm-8.1.0.dist-info/top_level.txt,sha256=kiu-91q3_rJLUoc2wl8_lC4cIlpgtgdD_4NaChF4hOA,15 +setuptools_scm/.git_archival.txt,sha256=2_90kdS1POSQMuZfBCUw6qNjObu7Ijp8DmptEAmlGkU,102 +setuptools_scm/__init__.py,sha256=k4jjJK8ejFI95amIoLWNCFECWIQW9NlxF9Had4RqOHM,785 +setuptools_scm/__main__.py,sha256=AhntzdNH3Jhcio_Ohoc6_EW7CuIN02OM-0irpGEXXh0,116 +setuptools_scm/__pycache__/__init__.cpython-312.pyc,, +setuptools_scm/__pycache__/__main__.cpython-312.pyc,, +setuptools_scm/__pycache__/_cli.cpython-312.pyc,, +setuptools_scm/__pycache__/_config.cpython-312.pyc,, +setuptools_scm/__pycache__/_entrypoints.cpython-312.pyc,, +setuptools_scm/__pycache__/_get_version_impl.cpython-312.pyc,, +setuptools_scm/__pycache__/_log.cpython-312.pyc,, +setuptools_scm/__pycache__/_modify_version.cpython-312.pyc,, +setuptools_scm/__pycache__/_overrides.cpython-312.pyc,, +setuptools_scm/__pycache__/_run_cmd.cpython-312.pyc,, +setuptools_scm/__pycache__/_types.cpython-312.pyc,, +setuptools_scm/__pycache__/_version_cls.cpython-312.pyc,, +setuptools_scm/__pycache__/discover.cpython-312.pyc,, +setuptools_scm/__pycache__/fallbacks.cpython-312.pyc,, +setuptools_scm/__pycache__/git.cpython-312.pyc,, +setuptools_scm/__pycache__/hg.cpython-312.pyc,, +setuptools_scm/__pycache__/hg_git.cpython-312.pyc,, +setuptools_scm/__pycache__/integration.cpython-312.pyc,, +setuptools_scm/__pycache__/scm_workdir.cpython-312.pyc,, +setuptools_scm/__pycache__/version.cpython-312.pyc,, +setuptools_scm/_cli.py,sha256=c1OtDXCGl7IzCdZ8z8yJg9TEBt1eXjh3mgqr16Qc5dQ,5597 +setuptools_scm/_config.py,sha256=Zf9jOxr7BWQ8RN863MPOfzO6x8Os_pwYi7VuA0Bbr4U,4968 +setuptools_scm/_entrypoints.py,sha256=GX3Lqs4YFUbQMa0mKCVYpXsfP8kR8nIuYbXRYkPaz7Y,3843 +setuptools_scm/_file_finders/__init__.py,sha256=QBZkrT7FLNfclH7taOohnNAPkVtaLJva3cHpX0sozbc,3751 +setuptools_scm/_file_finders/__pycache__/__init__.cpython-312.pyc,, +setuptools_scm/_file_finders/__pycache__/git.cpython-312.pyc,, +setuptools_scm/_file_finders/__pycache__/hg.cpython-312.pyc,, +setuptools_scm/_file_finders/__pycache__/pathtools.cpython-312.pyc,, +setuptools_scm/_file_finders/git.py,sha256=vW_SGVGTXNFyC4dHOhNjh3ER2Aqp0QNe1qc0XLUIZEQ,4156 +setuptools_scm/_file_finders/hg.py,sha256=1I4LPr_k7lIVMTqSl9_h1_csIEYMfRow2ta5FEo0Rjs,2246 +setuptools_scm/_file_finders/pathtools.py,sha256=AgOl5u_WHxCQeiUCwlN8bUE3B4vs5BxSJEK1LJutyus,179 +setuptools_scm/_get_version_impl.py,sha256=P72m2imnte-J7IXD3nTjYk7gBUoLk6hrZZv5O5r9akY,5940 +setuptools_scm/_integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools_scm/_integration/__pycache__/__init__.cpython-312.pyc,, +setuptools_scm/_integration/__pycache__/dump_version.cpython-312.pyc,, +setuptools_scm/_integration/__pycache__/pyproject_reading.cpython-312.pyc,, +setuptools_scm/_integration/__pycache__/setuptools.cpython-312.pyc,, +setuptools_scm/_integration/__pycache__/toml.cpython-312.pyc,, +setuptools_scm/_integration/dump_version.py,sha256=2VYFa0Aa1YRcrQQVit-COg7w1QaRjmZFM22LYZWuvEg,2584 +setuptools_scm/_integration/pyproject_reading.py,sha256=Wz-_ncjggrC-YfCsclIsSd2BkH0OYLdQhl9Insobpcg,2604 +setuptools_scm/_integration/setuptools.py,sha256=DcYqpUUMMu_gD2XxdyY3enPFr12pqV9fZa06PuFqBSU,3468 +setuptools_scm/_integration/toml.py,sha256=GYXDZOUC-AfQ_WmOIESP1ARBnc6WKIBQy3oDvsF-bUQ,1467 +setuptools_scm/_log.py,sha256=BFsXD-s2fcLV3zZaSQkQfRKPS-MjcI0tsmthJ3a2HqA,2137 +setuptools_scm/_modify_version.py,sha256=9VU-juFg2IZjrcyz9kLGRfBq4RyZZElhjPMipqjB3Xc,1738 +setuptools_scm/_overrides.py,sha256=2-ld0PSoi8IPVUAS3H2aBWM7yxJneDcyydq9ueQRdsE,1655 +setuptools_scm/_run_cmd.py,sha256=6AB6Kv1kg75Yx6-p7NHMrXSWTCPWDAN5id8cSF8XW6s,6094 +setuptools_scm/_types.py,sha256=QhY9jqXTH8sSxHXNQg2toQduk60s0vXya_Mdfv5rQAg,708 +setuptools_scm/_version_cls.py,sha256=9wEWl4WY_sUvqZhN1PQbVETVq9OAQKu1Y47ZPg9vx-c,2925 +setuptools_scm/discover.py,sha256=Kfm8S5I078vw8Cvbs9qpKOg5dr2TqBp1us-W579Dlts,2027 +setuptools_scm/fallbacks.py,sha256=x3Xv1p89AqJiBX6oxuoo8Di0yR5ijOFOwKBJGAeWTbY,1448 +setuptools_scm/git.py,sha256=-FbDzrVeEYd6jgEEzpWfuaeIfksS2QPktHZINXeSyzA,10526 +setuptools_scm/hg.py,sha256=iY294X0ZOqLEv_KXmPWwXXsPDhgx4wsOVoxFkM9WiPc,6207 +setuptools_scm/hg_git.py,sha256=hx2rq1kwW9Zs23xK9ZCn509emze22iyXvT3zIZh7JFM,4546 +setuptools_scm/integration.py,sha256=0l04N6IhRKW32vD9DPvvJuiZ1HrVCYupmNYDcYXu8lQ,806 +setuptools_scm/scm_workdir.py,sha256=oreoRhJfvhhxVGIhDPWH8-dg9umsmZM0sV2oRIzGXc8,327 +setuptools_scm/version.py,sha256=TzdIjApX5t-aWBwGZoJLykpGWVVStRxH2Fui0RVVXvU,14215 diff --git a/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/REQUESTED b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/REQUESTED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/WHEEL b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/WHEEL new file mode 100644 index 0000000000..bab98d6758 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.43.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/entry_points.txt b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/entry_points.txt new file mode 100644 index 0000000000..2212baa8d1 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/entry_points.txt @@ -0,0 +1,42 @@ +[distutils.setup_keywords] +use_scm_version = setuptools_scm._integration.setuptools:version_keyword + +[setuptools.file_finders] +setuptools_scm = setuptools_scm._file_finders:find_files + +[setuptools.finalize_distribution_options] +setuptools_scm = setuptools_scm._integration.setuptools:infer_version + +[setuptools_scm.files_command] +.git = setuptools_scm._file_finders.git:git_find_files +.hg = setuptools_scm._file_finders.hg:hg_find_files + +[setuptools_scm.files_command_fallback] +.git_archival.txt = setuptools_scm._file_finders.git:git_archive_find_files +.hg_archival.txt = setuptools_scm._file_finders.hg:hg_archive_find_files + +[setuptools_scm.local_scheme] +dirty-tag = setuptools_scm.version:get_local_dirty_tag +no-local-version = setuptools_scm.version:get_no_local_node +node-and-date = setuptools_scm.version:get_local_node_and_date +node-and-timestamp = setuptools_scm.version:get_local_node_and_timestamp + +[setuptools_scm.parse_scm] +.git = setuptools_scm.git:parse +.hg = setuptools_scm.hg:parse + +[setuptools_scm.parse_scm_fallback] +.git_archival.txt = setuptools_scm.git:parse_archival +.hg_archival.txt = setuptools_scm.hg:parse_archival +PKG-INFO = setuptools_scm.fallbacks:parse_pkginfo +pyproject.toml = setuptools_scm.fallbacks:fallback_version +setup.py = setuptools_scm.fallbacks:fallback_version + +[setuptools_scm.version_scheme] +calver-by-date = setuptools_scm.version:calver_by_date +guess-next-dev = setuptools_scm.version:guess_next_dev_version +no-guess-dev = setuptools_scm.version:no_guess_dev_version +only-version = setuptools_scm.version:only_version +post-release = setuptools_scm.version:postrelease_version +python-simplified-semver = setuptools_scm.version:simplified_semver_version +release-branch-semver = setuptools_scm.version:release_branch_semver_version diff --git a/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/top_level.txt b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/top_level.txt new file mode 100644 index 0000000000..cba8d88607 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm-8.1.0.dist-info/top_level.txt @@ -0,0 +1 @@ +setuptools_scm diff --git a/setuptools/_build_vendor/setuptools_scm/.git_archival.txt b/setuptools/_build_vendor/setuptools_scm/.git_archival.txt new file mode 100644 index 0000000000..7c5100942a --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/.git_archival.txt @@ -0,0 +1,3 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ diff --git a/setuptools/_build_vendor/setuptools_scm/__init__.py b/setuptools/_build_vendor/setuptools_scm/__init__.py new file mode 100644 index 0000000000..e265e859ca --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/__init__.py @@ -0,0 +1,30 @@ +""" +:copyright: 2010-2023 by Ronny Pfannschmidt +:license: MIT +""" + +from __future__ import annotations + +from ._config import DEFAULT_LOCAL_SCHEME +from ._config import DEFAULT_VERSION_SCHEME +from ._config import Configuration +from ._get_version_impl import _get_version +from ._get_version_impl import get_version +from ._integration.dump_version import dump_version # soft deprecated +from ._version_cls import NonNormalizedVersion +from ._version_cls import Version +from .version import ScmVersion + +# Public API +__all__ = [ + "DEFAULT_LOCAL_SCHEME", + "DEFAULT_VERSION_SCHEME", + "Configuration", + "NonNormalizedVersion", + "ScmVersion", + "Version", + "_get_version", + "dump_version", + # soft deprecated imports, left for backward compatibility + "get_version", +] diff --git a/setuptools/_build_vendor/setuptools_scm/__main__.py b/setuptools/_build_vendor/setuptools_scm/__main__.py new file mode 100644 index 0000000000..3f56d42a0c --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/__main__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from ._cli import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/setuptools/_build_vendor/setuptools_scm/_cli.py b/setuptools/_build_vendor/setuptools_scm/_cli.py new file mode 100644 index 0000000000..8c815237a0 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_cli.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import argparse +import json +import os +import sys + +from typing import Any + +from setuptools_scm import Configuration +from setuptools_scm._file_finders import find_files +from setuptools_scm._get_version_impl import _get_version +from setuptools_scm.discover import walk_potential_roots + + +def main(args: list[str] | None = None) -> int: + opts = _get_cli_opts(args) + inferred_root: str = opts.root or "." + + pyproject = opts.config or _find_pyproject(inferred_root) + + try: + config = Configuration.from_file( + pyproject, + root=(os.path.abspath(opts.root) if opts.root is not None else None), + ) + except (LookupError, FileNotFoundError) as ex: + # no pyproject.toml OR no [tool.setuptools_scm] + print( + f"Warning: could not use {os.path.relpath(pyproject)}," + " using default configuration.\n" + f" Reason: {ex}.", + file=sys.stderr, + ) + config = Configuration(root=inferred_root) + + version = _get_version( + config, force_write_version_files=opts.force_write_version_files + ) + if version is None: + raise SystemExit("ERROR: no version found for", opts) + if opts.strip_dev: + version = version.partition(".dev")[0] + + return command(opts, version, config) + + +def _get_cli_opts(args: list[str] | None) -> argparse.Namespace: + prog = "python -m setuptools_scm" + desc = "Print project version according to SCM metadata" + parser = argparse.ArgumentParser(prog, description=desc) + # By default, help for `--help` starts with lower case, so we keep the pattern: + parser.add_argument( + "-r", + "--root", + default=None, + help='directory managed by the SCM, default: inferred from config file, or "."', + ) + parser.add_argument( + "-c", + "--config", + default=None, + metavar="PATH", + help="path to 'pyproject.toml' with setuptools_scm config, " + "default: looked up in the current or parent directories", + ) + parser.add_argument( + "--strip-dev", + action="store_true", + help="remove the dev/local parts of the version before printing the version", + ) + parser.add_argument( + "-N", + "--no-version", + action="store_true", + help="do not include package version in the output", + ) + output_formats = ["json", "plain", "key-value"] + parser.add_argument( + "-f", + "--format", + type=str.casefold, + default="plain", + help="specify output format", + choices=output_formats, + ) + parser.add_argument( + "-q", + "--query", + type=str.casefold, + nargs="*", + help="display setuptools_scm settings according to query, " + "e.g. dist_name, do not supply an argument in order to " + "print a list of valid queries.", + ) + parser.add_argument( + "--force-write-version-files", + action="store_true", + help="trigger to write the content of the version files\n" + "its recommended to use normal/editable installation instead)", + ) + sub = parser.add_subparsers(title="extra commands", dest="command", metavar="") + # We avoid `metavar` to prevent printing repetitive information + desc = "List information about the package, e.g. included files" + sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc) + return parser.parse_args(args) + + +# flake8: noqa: C901 +def command(opts: argparse.Namespace, version: str, config: Configuration) -> int: + data: dict[str, Any] = {} + + if opts.command == "ls": + opts.query = ["files"] + + if opts.query == []: + opts.no_version = True + sys.stderr.write("Available queries:\n\n") + opts.query = ["queries"] + data["queries"] = ["files", *config.__dataclass_fields__] + + if opts.query is None: + opts.query = [] + + if not opts.no_version: + data["version"] = version + + if "files" in opts.query: + data["files"] = find_files(config.root) + + for q in opts.query: + if q in ["files", "queries", "version"]: + continue + + try: + if q.startswith("_"): + raise AttributeError() + data[q] = getattr(config, q) + except AttributeError: + sys.stderr.write(f"Error: unknown query: '{q}'\n") + return 1 + + if opts.format == "json": + print(json.dumps(data, indent=2)) + + if opts.format == "plain": + _print_plain(data) + + if opts.format == "key-value": + _print_key_value(data) + + return 0 + + +def _print_plain(data: dict[str, Any]) -> None: + version = data.pop("version", None) + if version: + print(version) + files = data.pop("files", []) + for file_ in files: + print(file_) + queries = data.pop("queries", []) + for query in queries: + print(query) + if data: + print("\n".join(data.values())) + + +def _print_key_value(data: dict[str, Any]) -> None: + for key, value in data.items(): + if isinstance(value, str): + print(f"{key} = {value}") + else: + str_value = "\n ".join(value) + print(f"{key} = {str_value}") + + +def _find_pyproject(parent: str) -> str: + for directory in walk_potential_roots(os.path.abspath(parent)): + pyproject = os.path.join(directory, "pyproject.toml") + if os.path.isfile(pyproject): + return pyproject + + return os.path.abspath( + "pyproject.toml" + ) # use default name to trigger the default errors diff --git a/setuptools/_build_vendor/setuptools_scm/_config.py b/setuptools/_build_vendor/setuptools_scm/_config.py new file mode 100644 index 0000000000..6ed520f933 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_config.py @@ -0,0 +1,152 @@ +"""configuration""" + +from __future__ import annotations + +import dataclasses +import os +import re +import warnings + +from pathlib import Path +from typing import Any +from typing import Pattern +from typing import Protocol + +from . import _log +from . import _types as _t +from ._integration.pyproject_reading import ( + get_args_for_pyproject as _get_args_for_pyproject, +) +from ._integration.pyproject_reading import read_pyproject as _read_pyproject +from ._overrides import read_toml_overrides +from ._version_cls import Version as _Version +from ._version_cls import _validate_version_cls +from ._version_cls import _VersionT + +log = _log.log.getChild("config") + +DEFAULT_TAG_REGEX = re.compile( + r"^(?:[\w-]+-)?(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$" +) +"""default tag regex that tries to match PEP440 style versions +with prefix consisting of dashed words""" + +DEFAULT_VERSION_SCHEME = "guess-next-dev" +DEFAULT_LOCAL_SCHEME = "node-and-date" + + +def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]: + if not value: + regex = DEFAULT_TAG_REGEX + else: + regex = re.compile(value) + + group_names = regex.groupindex.keys() + if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names): + warnings.warn( + "Expected tag_regex to contain a single match group or a group named" + " 'version' to identify the version part of any tag." + ) + + return regex + + +class ParseFunction(Protocol): + def __call__( + self, root: _t.PathT, *, config: Configuration + ) -> _t.SCMVERSION | None: ... + + +def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str: + log.debug("check absolute root=%s relative_to=%s", root, relative_to) + if relative_to: + if ( + os.path.isabs(root) + and os.path.isabs(relative_to) + and not os.path.commonpath([root, relative_to]) == root + ): + warnings.warn( + f"absolute root path '{root}' overrides relative_to '{relative_to}'" + ) + if os.path.isdir(relative_to): + warnings.warn( + "relative_to is expected to be a file," + f" its the directory {relative_to}\n" + "assuming the parent directory was passed" + ) + log.debug("dir %s", relative_to) + root = os.path.join(relative_to, root) + else: + log.debug("file %s", relative_to) + root = os.path.join(os.path.dirname(relative_to), root) + return os.path.abspath(root) + + +@dataclasses.dataclass +class Configuration: + """Global configuration model""" + + relative_to: _t.PathT | None = None + root: _t.PathT = "." + version_scheme: _t.VERSION_SCHEME = DEFAULT_VERSION_SCHEME + local_scheme: _t.VERSION_SCHEME = DEFAULT_LOCAL_SCHEME + tag_regex: Pattern[str] = DEFAULT_TAG_REGEX + parentdir_prefix_version: str | None = None + fallback_version: str | None = None + fallback_root: _t.PathT = "." + write_to: _t.PathT | None = None + write_to_template: str | None = None + version_file: _t.PathT | None = None + version_file_template: str | None = None + parse: ParseFunction | None = None + git_describe_command: _t.CMD_TYPE | None = None + dist_name: str | None = None + version_cls: type[_VersionT] = _Version + search_parent_directories: bool = False + + parent: _t.PathT | None = None + + @property + def absolute_root(self) -> str: + return _check_absolute_root(self.root, self.relative_to) + + @classmethod + def from_file( + cls, + name: str | os.PathLike[str] = "pyproject.toml", + dist_name: str | None = None, + _require_section: bool = True, + **kwargs: Any, + ) -> Configuration: + """ + Read Configuration from pyproject.toml (or similar). + Raises exceptions when file is not found or toml is + not installed or the file has invalid format or does + not contain the [tool.setuptools_scm] section. + """ + + pyproject_data = _read_pyproject(Path(name), require_section=_require_section) + args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs) + + args.update(read_toml_overrides(args["dist_name"])) + relative_to = args.pop("relative_to", name) + return cls.from_data(relative_to=relative_to, data=args) + + @classmethod + def from_data( + cls, relative_to: str | os.PathLike[str], data: dict[str, Any] + ) -> Configuration: + """ + given configuration data + create a config instance after validating tag regex/version class + """ + tag_regex = _check_tag_regex(data.pop("tag_regex", None)) + version_cls = _validate_version_cls( + data.pop("version_cls", None), data.pop("normalize", True) + ) + return cls( + relative_to=relative_to, + version_cls=version_cls, + tag_regex=tag_regex, + **data, + ) diff --git a/setuptools/_build_vendor/setuptools_scm/_entrypoints.py b/setuptools/_build_vendor/setuptools_scm/_entrypoints.py new file mode 100644 index 0000000000..5eab1ed2b9 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_entrypoints.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import sys + +from typing import TYPE_CHECKING +from typing import Any +from typing import Callable +from typing import Iterator +from typing import cast +from typing import overload + +from . import _log +from . import version + +if TYPE_CHECKING: + from . import _types as _t + from ._config import Configuration + from ._config import ParseFunction + + +from importlib.metadata import EntryPoint as EntryPoint + +if sys.version_info[:2] < (3, 10): + from importlib.metadata import entry_points as legacy_entry_points + + class EntryPoints: + _groupdata: list[EntryPoint] + + def __init__(self, groupdata: list[EntryPoint]) -> None: + self._groupdata = groupdata + + def select(self, name: str) -> EntryPoints: + return EntryPoints([x for x in self._groupdata if x.name == name]) + + def __iter__(self) -> Iterator[EntryPoint]: + return iter(self._groupdata) + + def entry_points(group: str) -> EntryPoints: + return EntryPoints(legacy_entry_points()[group]) + +else: + from importlib.metadata import EntryPoints + from importlib.metadata import entry_points + + +log = _log.log.getChild("entrypoints") + + +def version_from_entrypoint( + config: Configuration, *, entrypoint: str, root: _t.PathT +) -> version.ScmVersion | None: + from .discover import iter_matching_entrypoints + + log.debug("version_from_ep %s in %s", entrypoint, root) + for ep in iter_matching_entrypoints(root, entrypoint, config): + fn: ParseFunction = ep.load() + maybe_version: version.ScmVersion | None = fn(root, config=config) + log.debug("%s found %r", ep, maybe_version) + if maybe_version is not None: + return maybe_version + return None + + +def iter_entry_points(group: str, name: str | None = None) -> Iterator[EntryPoint]: + eps: EntryPoints = entry_points(group=group) + res = eps if name is None else eps.select(name=name) + + return iter(res) + + +def _get_ep(group: str, name: str) -> Any | None: + for ep in iter_entry_points(group, name): + log.debug("ep found: %s", ep.name) + return ep.load() + else: + return None + + +def _get_from_object_reference_str(path: str, group: str) -> Any | None: + # todo: remove for importlib native spelling + ep = EntryPoint(path, path, group) + try: + return ep.load() + except (AttributeError, ModuleNotFoundError): + return None + + +def _iter_version_schemes( + entrypoint: str, + scheme_value: _t.VERSION_SCHEMES, + _memo: set[object] | None = None, +) -> Iterator[Callable[[version.ScmVersion], str]]: + if _memo is None: + _memo = set() + if isinstance(scheme_value, str): + scheme_value = cast( + "_t.VERSION_SCHEMES", + _get_ep(entrypoint, scheme_value) + or _get_from_object_reference_str(scheme_value, entrypoint), + ) + + if isinstance(scheme_value, (list, tuple)): + for variant in scheme_value: + if variant not in _memo: + _memo.add(variant) + yield from _iter_version_schemes(entrypoint, variant, _memo=_memo) + elif callable(scheme_value): + yield scheme_value + + +@overload +def _call_version_scheme( + version: version.ScmVersion, + entrypoint: str, + given_value: _t.VERSION_SCHEMES, + default: str, +) -> str: ... + + +@overload +def _call_version_scheme( + version: version.ScmVersion, + entrypoint: str, + given_value: _t.VERSION_SCHEMES, + default: None, +) -> str | None: ... + + +def _call_version_scheme( + version: version.ScmVersion, + entrypoint: str, + given_value: _t.VERSION_SCHEMES, + default: str | None, +) -> str | None: + for scheme in _iter_version_schemes(entrypoint, given_value): + result = scheme(version) + if result is not None: + return result + return default diff --git a/setuptools/_build_vendor/setuptools_scm/_file_finders/__init__.py b/setuptools/_build_vendor/setuptools_scm/_file_finders/__init__.py new file mode 100644 index 0000000000..8201bae171 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_file_finders/__init__.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import itertools +import os + +from typing import TYPE_CHECKING +from typing import Callable + +from .. import _log +from .. import _types as _t +from .._entrypoints import iter_entry_points +from .pathtools import norm_real + +if TYPE_CHECKING: + import sys + + if sys.version_info >= (3, 10): + from typing import TypeGuard + else: + from typing_extensions import TypeGuard + + +log = _log.log.getChild("file_finder") + + +def scm_find_files( + path: _t.PathT, + scm_files: set[str], + scm_dirs: set[str], + force_all_files: bool = False, +) -> list[str]: + """ setuptools compatible file finder that follows symlinks + + - path: the root directory from which to search + - scm_files: set of scm controlled files and symlinks + (including symlinks to directories) + - scm_dirs: set of scm controlled directories + (including directories containing no scm controlled files) + - force_all_files: ignore ``scm_files`` and ``scm_dirs`` and list everything. + + scm_files and scm_dirs must be absolute with symlinks resolved (realpath), + with normalized case (normcase) + + Spec here: https://setuptools.pypa.io/en/latest/userguide/extension.html#\ + adding-support-for-revision-control-systems + """ + realpath = norm_real(path) + seen: set[str] = set() + res: list[str] = [] + for dirpath, dirnames, filenames in os.walk(realpath, followlinks=True): + # dirpath with symlinks resolved + realdirpath = norm_real(dirpath) + + def _link_not_in_scm(n: str, realdirpath: str = realdirpath) -> bool: + fn = os.path.join(realdirpath, os.path.normcase(n)) + return os.path.islink(fn) and fn not in scm_files + + if not force_all_files and realdirpath not in scm_dirs: + # directory not in scm, don't walk it's content + dirnames[:] = [] + continue + if os.path.islink(dirpath) and not os.path.relpath( + realdirpath, realpath + ).startswith(os.pardir): + # a symlink to a directory not outside path: + # we keep it in the result and don't walk its content + res.append(os.path.join(path, os.path.relpath(dirpath, path))) + dirnames[:] = [] + continue + if realdirpath in seen: + # symlink loop protection + dirnames[:] = [] + continue + dirnames[:] = [ + dn for dn in dirnames if force_all_files or not _link_not_in_scm(dn) + ] + for filename in filenames: + if not force_all_files and _link_not_in_scm(filename): + continue + # dirpath + filename with symlinks preserved + fullfilename = os.path.join(dirpath, filename) + is_tracked = norm_real(fullfilename) in scm_files + if force_all_files or is_tracked: + res.append(os.path.join(path, os.path.relpath(fullfilename, realpath))) + seen.add(realdirpath) + return res + + +def is_toplevel_acceptable(toplevel: str | None) -> TypeGuard[str]: + """ """ + if toplevel is None: + return False + + ignored: list[str] = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split( + os.pathsep + ) + ignored = [os.path.normcase(p) for p in ignored] + + log.debug("toplevel: %r\n ignored %s", toplevel, ignored) + + return toplevel not in ignored + + +def find_files(path: _t.PathT = "") -> list[str]: + for ep in itertools.chain( + iter_entry_points("setuptools_scm.files_command"), + iter_entry_points("setuptools_scm.files_command_fallback"), + ): + command: Callable[[_t.PathT], list[str]] = ep.load() + res: list[str] = command(path) + if res: + return res + return [] diff --git a/setuptools/_build_vendor/setuptools_scm/_file_finders/git.py b/setuptools/_build_vendor/setuptools_scm/_file_finders/git.py new file mode 100644 index 0000000000..7b23f88642 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_file_finders/git.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import logging +import os +import subprocess +import tarfile + +from typing import IO + +from .. import _types as _t +from .._run_cmd import run as _run +from ..integration import data_from_mime +from . import is_toplevel_acceptable +from . import scm_find_files +from .pathtools import norm_real + +log = logging.getLogger(__name__) + + +def _git_toplevel(path: str) -> str | None: + try: + cwd = os.path.abspath(path or ".") + res = _run(["git", "rev-parse", "HEAD"], cwd=cwd) + if res.returncode: + # BAIL if there is no commit + log.error("listing git files failed - pretending there aren't any") + return None + res = _run( + ["git", "rev-parse", "--show-prefix"], + cwd=cwd, + ) + if res.returncode: + return None + out = res.stdout[:-1] # remove the trailing pathsep + if not out: + out = cwd + else: + # Here, ``out`` is a relative path to root of git. + # ``cwd`` is absolute path to current working directory. + # the below method removes the length of ``out`` from + # ``cwd``, which gives the git toplevel + assert cwd.replace("\\", "/").endswith(out), f"cwd={cwd!r}\nout={out!r}" + # In windows cwd contains ``\`` which should be replaced by ``/`` + # for this assertion to work. Length of string isn't changed by replace + # ``\\`` is just and escape for `\` + out = cwd[: -len(out)] + log.debug("find files toplevel %s", out) + return norm_real(out) + except subprocess.CalledProcessError: + # git returned error, we are not in a git repo + return None + except OSError: + # git command not found, probably + return None + + +def _git_interpret_archive(fd: IO[bytes], toplevel: str) -> tuple[set[str], set[str]]: + with tarfile.open(fileobj=fd, mode="r|*") as tf: + git_files = set() + git_dirs = {toplevel} + for member in tf.getmembers(): + name = os.path.normcase(member.name).replace("/", os.path.sep) + if member.type == tarfile.DIRTYPE: + git_dirs.add(name) + else: + git_files.add(name) + return git_files, git_dirs + + +def _git_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: + # use git archive instead of git ls-file to honor + # export-ignore git attribute + + cmd = ["git", "archive", "--prefix", toplevel + os.path.sep, "HEAD"] + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, cwd=toplevel, stderr=subprocess.DEVNULL + ) + assert proc.stdout is not None + try: + try: + return _git_interpret_archive(proc.stdout, toplevel) + finally: + # ensure we avoid resource warnings by cleaning up the process + proc.stdout.close() + proc.terminate() + except Exception: + if proc.wait() != 0: + log.error("listing git files failed - pretending there aren't any") + return set(), set() + + +def git_find_files(path: _t.PathT = "") -> list[str]: + toplevel = _git_toplevel(os.fspath(path)) + if not is_toplevel_acceptable(toplevel): + return [] + fullpath = norm_real(path) + if not fullpath.startswith(toplevel): + log.warning("toplevel mismatch computed %s vs resolved %s ", toplevel, fullpath) + git_files, git_dirs = _git_ls_files_and_dirs(toplevel) + return scm_find_files(path, git_files, git_dirs) + + +def git_archive_find_files(path: _t.PathT = "") -> list[str]: + # This function assumes that ``path`` is obtained from a git archive + # and therefore all the files that should be ignored were already removed. + archival = os.path.join(path, ".git_archival.txt") + if not os.path.exists(archival): + return [] + + data = data_from_mime(archival) + + if "$Format" in data.get("node", ""): + # Substitutions have not been performed, so not a reliable archive + return [] + + log.warning("git archive detected - fallback to listing all files") + return scm_find_files(path, set(), set(), force_all_files=True) diff --git a/setuptools/_build_vendor/setuptools_scm/_file_finders/hg.py b/setuptools/_build_vendor/setuptools_scm/_file_finders/hg.py new file mode 100644 index 0000000000..9115a5fa8a --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_file_finders/hg.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import logging +import os +import subprocess + +from .. import _types as _t +from .._file_finders import is_toplevel_acceptable +from .._file_finders import scm_find_files +from .._run_cmd import run as _run +from ..integration import data_from_mime +from .pathtools import norm_real + +log = logging.getLogger(__name__) + + +def _hg_toplevel(path: str) -> str | None: + try: + return _run( + ["hg", "root"], + cwd=(path or "."), + check=True, + ).parse_success(norm_real) + except subprocess.CalledProcessError: + # hg returned error, we are not in a mercurial repo + return None + except OSError: + # hg command not found, probably + return None + + +def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: + hg_files: set[str] = set() + hg_dirs = {toplevel} + res = _run(["hg", "files"], cwd=toplevel) + if res.returncode: + return set(), set() + for name in res.stdout.splitlines(): + name = os.path.normcase(name).replace("/", os.path.sep) + fullname = os.path.join(toplevel, name) + hg_files.add(fullname) + dirname = os.path.dirname(fullname) + while len(dirname) > len(toplevel) and dirname not in hg_dirs: + hg_dirs.add(dirname) + dirname = os.path.dirname(dirname) + return hg_files, hg_dirs + + +def hg_find_files(path: str = "") -> list[str]: + toplevel = _hg_toplevel(path) + if not is_toplevel_acceptable(toplevel): + return [] + assert toplevel is not None + hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel) + return scm_find_files(path, hg_files, hg_dirs) + + +def hg_archive_find_files(path: _t.PathT = "") -> list[str]: + # This function assumes that ``path`` is obtained from a mercurial archive + # and therefore all the files that should be ignored were already removed. + archival = os.path.join(path, ".hg_archival.txt") + if not os.path.exists(archival): + return [] + + data = data_from_mime(archival) + + if "node" not in data: + # Ensure file is valid + return [] + + log.warning("hg archive detected - fallback to listing all files") + return scm_find_files(path, set(), set(), force_all_files=True) diff --git a/setuptools/_build_vendor/setuptools_scm/_file_finders/pathtools.py b/setuptools/_build_vendor/setuptools_scm/_file_finders/pathtools.py new file mode 100644 index 0000000000..6de850897b --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_file_finders/pathtools.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import os + +from setuptools_scm import _types as _t + + +def norm_real(path: _t.PathT) -> str: + return os.path.normcase(os.path.realpath(path)) diff --git a/setuptools/_build_vendor/setuptools_scm/_get_version_impl.py b/setuptools/_build_vendor/setuptools_scm/_get_version_impl.py new file mode 100644 index 0000000000..877804c18b --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_get_version_impl.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import logging +import re +import warnings + +from pathlib import Path +from typing import Any +from typing import NoReturn +from typing import Pattern + +from . import _config +from . import _entrypoints +from . import _run_cmd +from . import _types as _t +from ._config import Configuration +from ._overrides import _read_pretended_version_for +from ._version_cls import _validate_version_cls +from .version import ScmVersion +from .version import format_version as _format_version + +EMPTY_TAG_REGEX_DEPRECATION = DeprecationWarning( + "empty regex for tag regex is invalid, using default" +) + +_log = logging.getLogger(__name__) + + +def parse_scm_version(config: Configuration) -> ScmVersion | None: + try: + if config.parse is not None: + parse_result = config.parse(config.absolute_root, config=config) + if parse_result is not None and not isinstance(parse_result, ScmVersion): + raise TypeError( + f"version parse result was {str!r}\n" + "please return a parsed version (ScmVersion)" + ) + return parse_result + else: + return _entrypoints.version_from_entrypoint( + config, + entrypoint="setuptools_scm.parse_scm", + root=config.absolute_root, + ) + except _run_cmd.CommandNotFoundError as e: + _log.exception("command %s not found while parsing the scm, using fallbacks", e) + return None + + +def parse_fallback_version(config: Configuration) -> ScmVersion | None: + return _entrypoints.version_from_entrypoint( + config, + entrypoint="setuptools_scm.parse_scm_fallback", + root=config.fallback_root, + ) + + +def parse_version(config: Configuration) -> ScmVersion | None: + return ( + _read_pretended_version_for(config) + or parse_scm_version(config) + or parse_fallback_version(config) + ) + + +def write_version_files( + config: Configuration, version: str, scm_version: ScmVersion +) -> None: + if config.write_to is not None: + from ._integration.dump_version import dump_version + + dump_version( + root=config.root, + version=version, + scm_version=scm_version, + write_to=config.write_to, + template=config.write_to_template, + ) + if config.version_file: + from ._integration.dump_version import write_version_to_path + + version_file = Path(config.version_file) + assert not version_file.is_absolute(), f"{version_file=}" + # todo: use a better name than fallback root + assert config.relative_to is not None + target = Path(config.relative_to).parent.joinpath(version_file) + write_version_to_path( + target, + template=config.version_file_template, + version=version, + scm_version=scm_version, + ) + + +def _get_version( + config: Configuration, force_write_version_files: bool | None = None +) -> str | None: + parsed_version = parse_version(config) + if parsed_version is None: + return None + version_string = _format_version(parsed_version) + if force_write_version_files is None: + force_write_version_files = True + warnings.warn( + "force_write_version_files ought to be set," + " presuming the legacy True value", + DeprecationWarning, + ) + + if force_write_version_files: + write_version_files(config, version=version_string, scm_version=parsed_version) + + return version_string + + +def _version_missing(config: Configuration) -> NoReturn: + raise LookupError( + f"setuptools-scm was unable to detect version for {config.absolute_root}.\n\n" + "Make sure you're either building from a fully intact git repository " + "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a " + "git checkout without the .git folder) don't contain the necessary " + "metadata and will not work.\n\n" + "For example, if you're using pip, instead of " + "https://github.com/user/proj/archive/master.zip " + "use git+https://github.com/user/proj.git#egg=proj" + ) + + +def get_version( + root: _t.PathT = ".", + version_scheme: _t.VERSION_SCHEME = _config.DEFAULT_VERSION_SCHEME, + local_scheme: _t.VERSION_SCHEME = _config.DEFAULT_LOCAL_SCHEME, + write_to: _t.PathT | None = None, + write_to_template: str | None = None, + version_file: _t.PathT | None = None, + version_file_template: str | None = None, + relative_to: _t.PathT | None = None, + tag_regex: str | Pattern[str] = _config.DEFAULT_TAG_REGEX, + parentdir_prefix_version: str | None = None, + fallback_version: str | None = None, + fallback_root: _t.PathT = ".", + parse: Any | None = None, + git_describe_command: _t.CMD_TYPE | None = None, + dist_name: str | None = None, + version_cls: Any | None = None, + normalize: bool = True, + search_parent_directories: bool = False, +) -> str: + """ + If supplied, relative_to should be a file from which root may + be resolved. Typically called by a script or module that is not + in the root of the repository to direct setuptools_scm to the + root of the repository by supplying ``__file__``. + """ + + version_cls = _validate_version_cls(version_cls, normalize) + del normalize + tag_regex = parse_tag_regex(tag_regex) + config = Configuration(**locals()) + maybe_version = _get_version(config, force_write_version_files=True) + + if maybe_version is None: + _version_missing(config) + return maybe_version + + +def parse_tag_regex(tag_regex: str | Pattern[str]) -> Pattern[str]: + if isinstance(tag_regex, str): + if tag_regex == "": + warnings.warn(EMPTY_TAG_REGEX_DEPRECATION) + return _config.DEFAULT_TAG_REGEX + else: + return re.compile(tag_regex) + else: + return tag_regex diff --git a/setuptools/_build_vendor/setuptools_scm/_integration/__init__.py b/setuptools/_build_vendor/setuptools_scm/_integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/_build_vendor/setuptools_scm/_integration/dump_version.py b/setuptools/_build_vendor/setuptools_scm/_integration/dump_version.py new file mode 100644 index 0000000000..1901b1d3ba --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_integration/dump_version.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import warnings + +from pathlib import Path + +from .. import _types as _t +from .._log import log as parent_log +from .._version_cls import _version_as_tuple +from ..version import ScmVersion + +log = parent_log.getChild("dump_version") + +TEMPLATES = { + ".py": """\ +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = {version!r} +__version_tuple__ = version_tuple = {version_tuple!r} +""", + ".txt": "{version}", +} + + +def dump_version( + root: _t.PathT, + version: str, + write_to: _t.PathT, + template: str | None = None, + scm_version: ScmVersion | None = None, +) -> None: + assert isinstance(version, str) + root = Path(root) + write_to = Path(write_to) + if write_to.is_absolute(): + # trigger warning on escape + write_to.relative_to(root) + warnings.warn( + f"{write_to=!s} is a absolute path," + " please switch to using a relative version file", + DeprecationWarning, + ) + target = write_to + else: + target = Path(root).joinpath(write_to) + write_version_to_path( + target, template=template, version=version, scm_version=scm_version + ) + + +def _validate_template(target: Path, template: str | None) -> str: + if template == "": + warnings.warn(f"{template=} looks like a error, using default instead") + template = None + if template is None: + template = TEMPLATES.get(target.suffix) + + if template is None: + raise ValueError( + f"bad file format: {target.suffix!r} (of {target})\n" + "only *.txt and *.py have a default template" + ) + else: + return template + + +def write_version_to_path( + target: Path, template: str | None, version: str, scm_version: ScmVersion | None +) -> None: + final_template = _validate_template(target, template) + log.debug("dump %s into %s", version, target) + version_tuple = _version_as_tuple(version) + if scm_version is not None: + content = final_template.format( + version=version, + version_tuple=version_tuple, + scm_version=scm_version, + ) + else: + content = final_template.format(version=version, version_tuple=version_tuple) + + target.write_text(content, encoding="utf-8") diff --git a/setuptools/_build_vendor/setuptools_scm/_integration/pyproject_reading.py b/setuptools/_build_vendor/setuptools_scm/_integration/pyproject_reading.py new file mode 100644 index 0000000000..0e4f9aa11b --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_integration/pyproject_reading.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import warnings + +from pathlib import Path +from typing import NamedTuple + +from .. import _log +from .setuptools import read_dist_name_from_setup_cfg +from .toml import TOML_RESULT +from .toml import read_toml_content + +log = _log.log.getChild("pyproject_reading") + +_ROOT = "root" + + +class PyProjectData(NamedTuple): + path: Path + tool_name: str + project: TOML_RESULT + section: TOML_RESULT + + @property + def project_name(self) -> str | None: + return self.project.get("name") + + +def read_pyproject( + path: Path = Path("pyproject.toml"), + tool_name: str = "setuptools_scm", + require_section: bool = True, +) -> PyProjectData: + defn = read_toml_content(path, None if require_section else {}) + try: + section = defn.get("tool", {})[tool_name] + except LookupError as e: + error = f"{path} does not contain a tool.{tool_name} section" + if require_section: + raise LookupError(error) from e + else: + log.warning("toml section missing %r", error, exc_info=True) + section = {} + + project = defn.get("project", {}) + return PyProjectData(path, tool_name, project, section) + + +def get_args_for_pyproject( + pyproject: PyProjectData, + dist_name: str | None, + kwargs: TOML_RESULT, +) -> TOML_RESULT: + """drops problematic details and figures the distribution name""" + section = pyproject.section.copy() + kwargs = kwargs.copy() + if "relative_to" in section: + relative = section.pop("relative_to") + warnings.warn( + f"{pyproject.path}: at [tool.{pyproject.tool_name}]\n" + f"ignoring value relative_to={relative!r}" + " as its always relative to the config file" + ) + if "dist_name" in section: + if dist_name is None: + dist_name = section.pop("dist_name") + else: + assert dist_name == section["dist_name"] + section.pop("dist_name") + if dist_name is None: + # minimal pep 621 support for figuring the pretend keys + dist_name = pyproject.project_name + if dist_name is None: + dist_name = read_dist_name_from_setup_cfg() + if _ROOT in kwargs: + if kwargs[_ROOT] is None: + kwargs.pop(_ROOT, None) + elif _ROOT in section: + if section[_ROOT] != kwargs[_ROOT]: + warnings.warn( + f"root {section[_ROOT]} is overridden" + f" by the cli arg {kwargs[_ROOT]}" + ) + section.pop(_ROOT, None) + return {"dist_name": dist_name, **section, **kwargs} diff --git a/setuptools/_build_vendor/setuptools_scm/_integration/setuptools.py b/setuptools/_build_vendor/setuptools_scm/_integration/setuptools.py new file mode 100644 index 0000000000..dfc5a28643 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_integration/setuptools.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import logging +import os +import warnings + +from typing import Any +from typing import Callable + +import setuptools + +from .. import _config + +log = logging.getLogger(__name__) + + +def read_dist_name_from_setup_cfg( + input: str | os.PathLike[str] = "setup.cfg", +) -> str | None: + # minimal effort to read dist_name off setup.cfg metadata + import configparser + + parser = configparser.ConfigParser() + parser.read([input], encoding="utf-8") + dist_name = parser.get("metadata", "name", fallback=None) + return dist_name + + +def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: + if int(_version.split(".")[0]) < 61: + warnings.warn( + RuntimeWarning( + f""" +ERROR: setuptools=={_version} is used in combination with setuptools_scm>=8.x + +Your build configuration is incomplete and previously worked by accident! +setuptools_scm requires setuptools>=61 + +Suggested workaround if applicable: + - migrating from the deprecated setup_requires mechanism to pep517/518 + and using a pyproject.toml to declare build dependencies + which are reliably pre-installed before running the build tools +""" + ) + ) + + +def _assign_version( + dist: setuptools.Distribution, config: _config.Configuration +) -> None: + from .._get_version_impl import _get_version + from .._get_version_impl import _version_missing + + # todo: build time plugin + maybe_version = _get_version(config, force_write_version_files=True) + + if maybe_version is None: + _version_missing(config) + else: + assert dist.metadata.version is None + dist.metadata.version = maybe_version + + +_warn_on_old_setuptools() + + +def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None: + log.debug("%s %r", hook, vars(dist.metadata)) + + +def version_keyword( + dist: setuptools.Distribution, + keyword: str, + value: bool | dict[str, Any] | Callable[[], dict[str, Any]], +) -> None: + overrides: dict[str, Any] + if value is True: + overrides = {} + elif callable(value): + overrides = value() + else: + assert isinstance(value, dict), "version_keyword expects a dict or True" + overrides = value + + assert ( + "dist_name" not in overrides + ), "dist_name may not be specified in the setup keyword " + dist_name: str | None = dist.metadata.name + _log_hookstart("version_keyword", dist) + + if dist.metadata.version is not None: + warnings.warn(f"version of {dist_name} already set") + return + + if dist_name is None: + dist_name = read_dist_name_from_setup_cfg() + + config = _config.Configuration.from_file( + dist_name=dist_name, + _require_section=False, + **overrides, + ) + _assign_version(dist, config) + + +def infer_version(dist: setuptools.Distribution) -> None: + _log_hookstart("infer_version", dist) + log.debug("dist %s %s", id(dist), id(dist.metadata)) + if dist.metadata.version is not None: + return # metadata already added by hook + dist_name = dist.metadata.name + if dist_name is None: + dist_name = read_dist_name_from_setup_cfg() + if not os.path.isfile("pyproject.toml"): + return + if dist_name == "setuptools_scm": + return + try: + config = _config.Configuration.from_file(dist_name=dist_name) + except LookupError as e: + log.info(e, exc_info=True) + else: + _assign_version(dist, config) diff --git a/setuptools/_build_vendor/setuptools_scm/_integration/toml.py b/setuptools/_build_vendor/setuptools_scm/_integration/toml.py new file mode 100644 index 0000000000..8ca38d975a --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_integration/toml.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import sys + +from pathlib import Path +from typing import TYPE_CHECKING +from typing import Any +from typing import Callable +from typing import Dict +from typing import TypedDict +from typing import cast + +if sys.version_info >= (3, 11): + from tomllib import loads as load_toml +else: + from tomli import loads as load_toml + +if TYPE_CHECKING: + if sys.version_info >= (3, 10): + from typing import TypeAlias + else: + from typing_extensions import TypeAlias + +from .. import _log + +log = _log.log.getChild("toml") + +TOML_RESULT: TypeAlias = Dict[str, Any] +TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT] + + +def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RESULT: + try: + data = path.read_text(encoding="utf-8") + except FileNotFoundError: + if default is None: + raise + else: + log.debug("%s missing, presuming default %r", path, default) + return default + else: + return load_toml(data) + + +class _CheatTomlData(TypedDict): + cheat: dict[str, Any] + + +def load_toml_or_inline_map(data: str | None) -> dict[str, Any]: + """ + load toml data - with a special hack if only a inline map is given + """ + if not data: + return {} + elif data[0] == "{": + data = "cheat=" + data + loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data)) + return loaded["cheat"] + return load_toml(data) diff --git a/setuptools/_build_vendor/setuptools_scm/_log.py b/setuptools/_build_vendor/setuptools_scm/_log.py new file mode 100644 index 0000000000..7e4b7db7da --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_log.py @@ -0,0 +1,87 @@ +""" +logging helpers, supports vendoring +""" + +from __future__ import annotations + +import contextlib +import logging +import os +import sys + +from typing import IO +from typing import Iterator +from typing import Mapping + +log = logging.getLogger(__name__.rsplit(".", 1)[0]) +log.propagate = False + + +class AlwaysStdErrHandler(logging.StreamHandler): # type: ignore[type-arg] + def __init___(self) -> None: + super().__init__(sys.stderr) + + @property # type: ignore [override] + def stream(self) -> IO[str]: + return sys.stderr + + @stream.setter + def stream(self, value: IO[str]) -> None: + assert value is sys.stderr + + +def make_default_handler() -> logging.Handler: + try: + from rich.console import Console + + console = Console(stderr=True) + from rich.logging import RichHandler + + return RichHandler(console=console) + except ImportError: + handler = AlwaysStdErrHandler() + handler.setFormatter(logging.Formatter("%(levelname)s %(name)s %(message)s")) + return handler + + +_default_handler = make_default_handler() + +log.addHandler(_default_handler) + + +def _default_log_level(_env: Mapping[str, str] = os.environ) -> int: + val: str | None = _env.get("SETUPTOOLS_SCM_DEBUG") + return logging.WARN if val is None else logging.DEBUG + + +log.setLevel(_default_log_level()) + + +@contextlib.contextmanager +def defer_to_pytest() -> Iterator[None]: + log.propagate = True + old_level = log.level + log.setLevel(logging.NOTSET) + log.removeHandler(_default_handler) + try: + yield + finally: + log.addHandler(_default_handler) + log.propagate = False + log.setLevel(old_level) + + +@contextlib.contextmanager +def enable_debug(handler: logging.Handler = _default_handler) -> Iterator[None]: + log.addHandler(handler) + old_level = log.level + log.setLevel(logging.DEBUG) + old_handler_level = handler.level + handler.setLevel(logging.DEBUG) + try: + yield + finally: + log.setLevel(old_level) + handler.setLevel(old_handler_level) + if handler is not _default_handler: + log.removeHandler(handler) diff --git a/setuptools/_build_vendor/setuptools_scm/_modify_version.py b/setuptools/_build_vendor/setuptools_scm/_modify_version.py new file mode 100644 index 0000000000..aae41a632f --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_modify_version.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import re + +from . import _types as _t + + +def strip_local(version_string: str) -> str: + public = version_string.partition("+")[0] + return public + + +def _add_post(version: str) -> str: + if "post" in version: + raise ValueError( + f"{version} already is a post release, refusing to guess the update" + ) + return f"{version}.post1" + + +def _bump_dev(version: str) -> str | None: + if ".dev" not in version: + return None + + prefix, tail = version.rsplit(".dev", 1) + if tail != "0": + raise ValueError( + "choosing custom numbers for the `.devX` distance " + "is not supported.\n " + f"The {version} can't be bumped\n" + "Please drop the tag or create a new supported one ending in .dev0" + ) + return prefix + + +def _bump_regex(version: str) -> str: + match = re.match(r"(.*?)(\d+)$", version) + if match is None: + raise ValueError( + f"{version} does not end with a number to bump, " + "please correct or use a custom version scheme" + ) + else: + prefix, tail = match.groups() + return f"{prefix}{int(tail) + 1}" + + +def _format_local_with_time(version: _t.SCMVERSION, time_format: str) -> str: + if version.exact or version.node is None: + return version.format_choice( + "", "+d{time:{time_format}}", time_format=time_format + ) + else: + return version.format_choice( + "+{node}", "+{node}.d{time:{time_format}}", time_format=time_format + ) + + +def _dont_guess_next_version(tag_version: _t.SCMVERSION) -> str: + version = strip_local(str(tag_version.tag)) + return _bump_dev(version) or _add_post(version) diff --git a/setuptools/_build_vendor/setuptools_scm/_overrides.py b/setuptools/_build_vendor/setuptools_scm/_overrides.py new file mode 100644 index 0000000000..ee9269a719 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_overrides.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import os +import re + +from typing import Any + +from . import _config +from . import _log +from . import version +from ._integration.toml import load_toml_or_inline_map + +log = _log.log.getChild("overrides") + +PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION" +PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}" + + +def read_named_env( + *, tool: str = "SETUPTOOLS_SCM", name: str, dist_name: str | None +) -> str | None: + """ """ + if dist_name is not None: + # Normalize the dist name as per PEP 503. + normalized_dist_name = re.sub(r"[-_.]+", "-", dist_name) + env_var_dist_name = normalized_dist_name.replace("-", "_").upper() + val = os.environ.get(f"{tool}_{name}_FOR_{env_var_dist_name}") + if val is not None: + return val + return os.environ.get(f"{tool}_{name}") + + +def _read_pretended_version_for( + config: _config.Configuration, +) -> version.ScmVersion | None: + """read a a overridden version from the environment + + tries ``SETUPTOOLS_SCM_PRETEND_VERSION`` + and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME`` + """ + log.debug("dist name: %s", config.dist_name) + + pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name) + + if pretended: + # we use meta here since the pretended version + # must adhere to the pep to begin with + return version.meta(tag=pretended, preformatted=True, config=config) + else: + return None + + +def read_toml_overrides(dist_name: str | None) -> dict[str, Any]: + data = read_named_env(name="OVERRIDES", dist_name=dist_name) + return load_toml_or_inline_map(data) diff --git a/setuptools/_build_vendor/setuptools_scm/_run_cmd.py b/setuptools/_build_vendor/setuptools_scm/_run_cmd.py new file mode 100644 index 0000000000..f2a82852c3 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_run_cmd.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +import os +import shlex +import subprocess +import textwrap +import warnings + +from typing import TYPE_CHECKING +from typing import Callable +from typing import Final +from typing import Mapping +from typing import Sequence +from typing import TypeVar +from typing import overload + +from . import _log +from . import _types as _t + +if TYPE_CHECKING: + BaseCompletedProcess = subprocess.CompletedProcess[str] +else: + BaseCompletedProcess = subprocess.CompletedProcess + +# pick 40 seconds +# unfortunately github CI for windows sometimes needs +# up to 30 seconds to start a command + + +def _get_timeout(env: Mapping[str, str]) -> int: + return int(env.get("SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT") or 40) + + +BROKEN_TIMEOUT: Final[int] = _get_timeout(os.environ) + +log = _log.log.getChild("run_cmd") + +PARSE_RESULT = TypeVar("PARSE_RESULT") +T = TypeVar("T") + + +class CompletedProcess(BaseCompletedProcess): + @classmethod + def from_raw( + cls, input: BaseCompletedProcess, strip: bool = True + ) -> CompletedProcess: + return cls( + args=input.args, + returncode=input.returncode, + stdout=input.stdout.strip() if strip and input.stdout else input.stdout, + stderr=input.stderr.strip() if strip and input.stderr else input.stderr, + ) + + @overload + def parse_success( + self, + parse: Callable[[str], PARSE_RESULT], + default: None = None, + error_msg: str | None = None, + ) -> PARSE_RESULT | None: ... + + @overload + def parse_success( + self, + parse: Callable[[str], PARSE_RESULT], + default: T, + error_msg: str | None = None, + ) -> PARSE_RESULT | T: ... + + def parse_success( + self, + parse: Callable[[str], PARSE_RESULT], + default: T | None = None, + error_msg: str | None = None, + ) -> PARSE_RESULT | T | None: + if self.returncode: + if error_msg: + log.warning("%s %s", error_msg, self) + return default + else: + return parse(self.stdout) + + +def no_git_env(env: Mapping[str, str]) -> dict[str, str]: + # adapted from pre-commit + # Too many bugs dealing with environment variables and GIT: + # https://github.com/pre-commit/pre-commit/issues/300 + # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running + # pre-commit hooks + # In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE + # while running pre-commit hooks in submodules. + # GIT_DIR: Causes git clone to clone wrong thing + # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit + for k, v in env.items(): + if k.startswith("GIT_"): + log.debug("%s: %s", k, v) + return { + k: v + for k, v in env.items() + if not k.startswith("GIT_") + or k in ("GIT_EXEC_PATH", "GIT_SSH", "GIT_SSH_COMMAND") + } + + +def avoid_pip_isolation(env: Mapping[str, str]) -> dict[str, str]: + """ + pip build isolation can break Mercurial + (see https://github.com/pypa/pip/issues/10635) + + pip uses PYTHONNOUSERSITE and a path in PYTHONPATH containing "pip-build-env-". + """ + new_env = {k: v for k, v in env.items() if k != "PYTHONNOUSERSITE"} + if "PYTHONPATH" not in new_env: + return new_env + + new_env["PYTHONPATH"] = os.pathsep.join( + [ + path + for path in new_env["PYTHONPATH"].split(os.pathsep) + if "pip-build-env-" not in path + ] + ) + return new_env + + +def ensure_stripped_str(str_or_bytes: str | bytes) -> str: + if isinstance(str_or_bytes, str): + return str_or_bytes.strip() + else: + return str_or_bytes.decode("utf-8", "surrogateescape").strip() + + +def run( + cmd: _t.CMD_TYPE, + cwd: _t.PathT, + *, + strip: bool = True, + trace: bool = True, + timeout: int | None = None, + check: bool = False, +) -> CompletedProcess: + if isinstance(cmd, str): + cmd = shlex.split(cmd) + else: + cmd = [os.fspath(x) for x in cmd] + cmd_4_trace = " ".join(map(_unsafe_quote_for_display, cmd)) + log.debug("at %s\n $ %s ", cwd, cmd_4_trace) + if timeout is None: + timeout = BROKEN_TIMEOUT + res = subprocess.run( + cmd, + capture_output=True, + cwd=os.fspath(cwd), + env=dict( + avoid_pip_isolation(no_git_env(os.environ)), + # os.environ, + # try to disable i18n, but still allow UTF-8 encoded text. + LC_ALL="C.UTF-8", + LANGUAGE="", + HGPLAIN="1", + ), + text=True, + encoding="utf-8", + timeout=timeout, + ) + + res = CompletedProcess.from_raw(res, strip=strip) + if trace: + if res.stdout: + log.debug("out:\n%s", textwrap.indent(res.stdout, " ")) + if res.stderr: + log.debug("err:\n%s", textwrap.indent(res.stderr, " ")) + if res.returncode: + log.debug("ret: %s", res.returncode) + if check: + res.check_returncode() + return res + + +def _unsafe_quote_for_display(item: _t.PathT) -> str: + # give better results than shlex.join in our cases + text = os.fspath(item) + return text if all(c not in text for c in " {[:") else f'"{text}"' + + +def has_command( + name: str, args: Sequence[str] = ["version"], warn: bool = True +) -> bool: + try: + p = run([name, *args], cwd=".") + if p.returncode != 0: + log.error(f"Command '{name}' returned non-zero. This is stderr:") + log.error(p.stderr) + except OSError as e: + log.warning("command %s missing: %s", name, e) + res = False + except subprocess.TimeoutExpired as e: + log.warning("command %s timed out %s", name, e) + res = False + + else: + res = not p.returncode + if not res and warn: + warnings.warn("%r was not found" % name, category=RuntimeWarning) + return res + + +class CommandNotFoundError(LookupError, FileNotFoundError): + pass + + +def require_command(name: str) -> None: + if not has_command(name, warn=False): + raise CommandNotFoundError(name) diff --git a/setuptools/_build_vendor/setuptools_scm/_types.py b/setuptools/_build_vendor/setuptools_scm/_types.py new file mode 100644 index 0000000000..b655c76f29 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_types.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import os + +from typing import TYPE_CHECKING +from typing import Callable +from typing import List +from typing import Sequence +from typing import Tuple +from typing import Union + +if TYPE_CHECKING: + import sys + + if sys.version_info >= (3, 10): + from typing import TypeAlias + else: + from typing_extensions import TypeAlias + + from . import version + +PathT: TypeAlias = Union["os.PathLike[str]", str] + +CMD_TYPE: TypeAlias = Union[Sequence[PathT], str] + +VERSION_SCHEME: TypeAlias = Union[str, Callable[["version.ScmVersion"], str]] +VERSION_SCHEMES: TypeAlias = Union[List[str], Tuple[str, ...], VERSION_SCHEME] +SCMVERSION: TypeAlias = "version.ScmVersion" diff --git a/setuptools/_build_vendor/setuptools_scm/_version_cls.py b/setuptools/_build_vendor/setuptools_scm/_version_cls.py new file mode 100644 index 0000000000..bb89bbb125 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/_version_cls.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from typing import Type +from typing import Union +from typing import cast + +try: + from packaging.version import InvalidVersion + from packaging.version import Version as Version +except ImportError: + from setuptools.extern.packaging.version import InvalidVersion # type: ignore + from setuptools.extern.packaging.version import Version as Version # type: ignore +from . import _log + +log = _log.log.getChild("version_cls") + + +class NonNormalizedVersion(Version): + """A non-normalizing version handler. + + You can use this class to preserve version verification but skip normalization. + For example you can use this to avoid git release candidate version tags + ("1.0.0-rc1") to be normalized to "1.0.0rc1". Only use this if you fully + trust the version tags. + """ + + def __init__(self, version: str) -> None: + # parse and validate using parent + super().__init__(version) + + # store raw for str + self._raw_version = version + + def __str__(self) -> str: + # return the non-normalized version (parent returns the normalized) + return self._raw_version + + def __repr__(self) -> str: + # same pattern as parent + return f"" + + +def _version_as_tuple(version_str: str) -> tuple[int | str, ...]: + try: + parsed_version = Version(version_str) + except InvalidVersion as e: + log.error("failed to parse version %s: %s", e, version_str) + return (version_str,) + else: + version_fields: tuple[int | str, ...] = parsed_version.release + if parsed_version.dev is not None: + version_fields += (f"dev{parsed_version.dev}",) + if parsed_version.local is not None: + version_fields += (parsed_version.local,) + return version_fields + + +_VersionT = Union[Version, NonNormalizedVersion] + + +def import_name(name: str) -> object: + import importlib + + pkg_name, cls_name = name.rsplit(".", 1) + pkg = importlib.import_module(pkg_name) + return getattr(pkg, cls_name) + + +def _validate_version_cls( + version_cls: type[_VersionT] | str | None, normalize: bool +) -> type[_VersionT]: + if not normalize: + if version_cls is not None: + raise ValueError( + "Providing a custom `version_cls` is not permitted when " + "`normalize=False`" + ) + return NonNormalizedVersion + else: + # Use `version_cls` if provided, default to packaging or pkg_resources + if version_cls is None: + return Version + elif isinstance(version_cls, str): + try: + return cast(Type[_VersionT], import_name(version_cls)) + except: # noqa + raise ValueError( + f"Unable to import version_cls='{version_cls}'" + ) from None + else: + return version_cls diff --git a/setuptools/_build_vendor/setuptools_scm/discover.py b/setuptools/_build_vendor/setuptools_scm/discover.py new file mode 100644 index 0000000000..7c1be381f4 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/discover.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import os + +from pathlib import Path +from typing import Iterable +from typing import Iterator + +from . import _entrypoints +from . import _log +from . import _types as _t +from ._config import Configuration + +log = _log.log.getChild("discover") + + +def walk_potential_roots(root: _t.PathT, search_parents: bool = True) -> Iterator[Path]: + """ + Iterate though a path and each of its parents. + :param root: File path. + :param search_parents: If ``False`` the parents are not considered. + """ + root = Path(root) + yield root + if search_parents: + yield from root.parents + + +def match_entrypoint(root: _t.PathT, name: str) -> bool: + """ + Consider a ``root`` as entry-point. + :param root: File path. + :param name: Subdirectory name. + :return: ``True`` if a subdirectory ``name`` exits in ``root``. + """ + + if os.path.exists(os.path.join(root, name)): + if not os.path.isabs(name): + return True + log.debug("ignoring bad ep %s", name) + + return False + + +# blocked entrypints from legacy plugins +_BLOCKED_EP_TARGETS = {"setuptools_scm_git_archive:parse"} + + +def iter_matching_entrypoints( + root: _t.PathT, entrypoint: str, config: Configuration +) -> Iterable[_entrypoints.EntryPoint]: + """ + Consider different entry-points in ``root`` and optionally its parents. + :param root: File path. + :param entrypoint: Entry-point to consider. + :param config: Configuration, + read ``search_parent_directories``, write found parent to ``parent``. + """ + + log.debug("looking for ep %s in %s", entrypoint, root) + from ._entrypoints import iter_entry_points + + for wd in walk_potential_roots(root, config.search_parent_directories): + for ep in iter_entry_points(entrypoint): + if ep.value in _BLOCKED_EP_TARGETS: + continue + if match_entrypoint(wd, ep.name): + log.debug("found ep %s in %s", ep, wd) + config.parent = wd + yield ep diff --git a/setuptools/_build_vendor/setuptools_scm/fallbacks.py b/setuptools/_build_vendor/setuptools_scm/fallbacks.py new file mode 100644 index 0000000000..45a75351c9 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/fallbacks.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import logging +import os + +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import _types as _t +from . import Configuration +from .integration import data_from_mime +from .version import ScmVersion +from .version import meta +from .version import tag_to_version + +log = logging.getLogger(__name__) + +_UNKNOWN = "UNKNOWN" + + +def parse_pkginfo(root: _t.PathT, config: Configuration) -> ScmVersion | None: + pkginfo = Path(root) / "PKG-INFO" + log.debug("pkginfo %s", pkginfo) + data = data_from_mime(pkginfo) + version = data.get("Version", _UNKNOWN) + if version != _UNKNOWN: + return meta(version, preformatted=True, config=config) + else: + return None + + +def fallback_version(root: _t.PathT, config: Configuration) -> ScmVersion | None: + if config.parentdir_prefix_version is not None: + _, parent_name = os.path.split(os.path.abspath(root)) + if parent_name.startswith(config.parentdir_prefix_version): + version = tag_to_version( + parent_name[len(config.parentdir_prefix_version) :], config + ) + if version is not None: + return meta(str(version), preformatted=True, config=config) + if config.fallback_version is not None: + log.debug("FALLBACK %s", config.fallback_version) + return meta(config.fallback_version, preformatted=True, config=config) + return None diff --git a/setuptools/_build_vendor/setuptools_scm/git.py b/setuptools/_build_vendor/setuptools_scm/git.py new file mode 100644 index 0000000000..eb1d519ab1 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/git.py @@ -0,0 +1,340 @@ +from __future__ import annotations + +import dataclasses +import logging +import os +import re +import shlex +import sys +import warnings + +from datetime import date +from datetime import datetime +from datetime import timezone +from os.path import samefile +from pathlib import Path +from typing import TYPE_CHECKING +from typing import Callable +from typing import Sequence + +from . import Configuration +from . import _types as _t +from . import discover +from ._run_cmd import CompletedProcess as _CompletedProcess +from ._run_cmd import require_command as _require_command +from ._run_cmd import run as _run +from .integration import data_from_mime +from .scm_workdir import Workdir +from .version import ScmVersion +from .version import meta +from .version import tag_to_version + +if TYPE_CHECKING: + from . import hg_git +log = logging.getLogger(__name__) + +REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b") +DESCRIBE_UNSUPPORTED = "%(describe" + +# If testing command in shell make sure to quote the match argument like +# '*[0-9]*' as it will expand before being sent to git if there are any matching +# files in current directory. +DEFAULT_DESCRIBE = [ + "git", + "describe", + "--dirty", + "--tags", + "--long", + "--match", + "*[0-9]*", +] + + +def run_git( + args: Sequence[str | os.PathLike[str]], + repo: Path, + *, + check: bool = False, + timeout: int | None = None, +) -> _CompletedProcess: + return _run( + ["git", "--git-dir", repo / ".git", *args], + cwd=repo, + check=check, + timeout=timeout, + ) + + +class GitWorkdir(Workdir): + """experimental, may change at any time""" + + @classmethod + def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None: + wd = Path(wd).resolve() + real_wd = run_git(["rev-parse", "--show-prefix"], wd).parse_success(parse=str) + if real_wd is None: + return None + else: + real_wd = real_wd[:-1] # remove the trailing pathsep + + if not real_wd: + real_wd = os.fspath(wd) + else: + str_wd = os.fspath(wd) + assert str_wd.replace("\\", "/").endswith(real_wd) + # In windows wd contains ``\`` which should be replaced by ``/`` + # for this assertion to work. Length of string isn't changed by replace + # ``\\`` is just and escape for `\` + real_wd = str_wd[: -len(real_wd)] + log.debug("real root %s", real_wd) + if not samefile(real_wd, wd): + return None + + return cls(Path(real_wd)) + + def is_dirty(self) -> bool: + return run_git( + ["status", "--porcelain", "--untracked-files=no"], self.path + ).parse_success( + parse=bool, + default=False, + ) + + def get_branch(self) -> str | None: + return run_git( + ["rev-parse", "--abbrev-ref", "HEAD"], + self.path, + ).parse_success( + parse=str, + error_msg="branch err (abbrev-err)", + ) or run_git( + ["symbolic-ref", "--short", "HEAD"], + self.path, + ).parse_success( + parse=str, + error_msg="branch err (symbolic-ref)", + ) + + def get_head_date(self) -> date | None: + def parse_timestamp(timestamp_text: str) -> date | None: + if "%c" in timestamp_text: + log.warning("git too old -> timestamp is %r", timestamp_text) + return None + if sys.version_info < (3, 11) and timestamp_text.endswith("Z"): + timestamp_text = timestamp_text[:-1] + "+00:00" + return datetime.fromisoformat(timestamp_text).date() + + res = run_git( + [ + *("-c", "log.showSignature=false"), + *("log", "-n", "1", "HEAD"), + "--format=%cI", + ], + self.path, + ) + return res.parse_success( + parse=parse_timestamp, + error_msg="logging the iso date for head failed", + ) + + def is_shallow(self) -> bool: + return self.path.joinpath(".git/shallow").is_file() + + def fetch_shallow(self) -> None: + run_git(["fetch", "--unshallow"], self.path, check=True, timeout=240) + + def node(self) -> str | None: + def _unsafe_short_node(node: str) -> str: + return node[:7] + + return run_git( + ["rev-parse", "--verify", "--quiet", "HEAD"], self.path + ).parse_success( + parse=_unsafe_short_node, + ) + + def count_all_nodes(self) -> int: + res = run_git(["rev-list", "HEAD"], self.path) + return res.stdout.count("\n") + 1 + + def default_describe(self) -> _CompletedProcess: + return run_git(DEFAULT_DESCRIBE[1:], self.path) + + +def warn_on_shallow(wd: GitWorkdir) -> None: + """experimental, may change at any time""" + if wd.is_shallow(): + warnings.warn(f'"{wd.path}" is shallow and may cause errors') + + +def fetch_on_shallow(wd: GitWorkdir) -> None: + """experimental, may change at any time""" + if wd.is_shallow(): + warnings.warn(f'"{wd.path}" was shallow, git fetch was used to rectify') + wd.fetch_shallow() + + +def fail_on_shallow(wd: GitWorkdir) -> None: + """experimental, may change at any time""" + if wd.is_shallow(): + raise ValueError( + f'{wd.path} is shallow, please correct with "git fetch --unshallow"' + ) + + +def get_working_directory(config: Configuration, root: _t.PathT) -> GitWorkdir | None: + """ + Return the working directory (``GitWorkdir``). + """ + + if config.parent: # todo broken + return GitWorkdir.from_potential_worktree(config.parent) + + for potential_root in discover.walk_potential_roots( + root, search_parents=config.search_parent_directories + ): + potential_wd = GitWorkdir.from_potential_worktree(potential_root) + if potential_wd is not None: + return potential_wd + + return GitWorkdir.from_potential_worktree(root) + + +def parse( + root: _t.PathT, + config: Configuration, + describe_command: str | list[str] | None = None, + pre_parse: Callable[[GitWorkdir], None] = warn_on_shallow, +) -> ScmVersion | None: + """ + :param pre_parse: experimental pre_parse action, may change at any time + """ + _require_command("git") + wd = get_working_directory(config, root) + if wd: + return _git_parse_inner( + config, wd, describe_command=describe_command, pre_parse=pre_parse + ) + else: + return None + + +def version_from_describe( + wd: GitWorkdir | hg_git.GitWorkdirHgClient, + config: Configuration, + describe_command: _t.CMD_TYPE | None, +) -> ScmVersion | None: + pass + + if config.git_describe_command is not None: + describe_command = config.git_describe_command + + if describe_command is not None: + if isinstance(describe_command, str): + describe_command = shlex.split(describe_command) + # todo: figure how to ensure git with gitdir gets correctly invoked + if describe_command[0] == "git": + describe_res = run_git(describe_command[1:], wd.path) + else: + describe_res = _run(describe_command, wd.path) + else: + describe_res = wd.default_describe() + + def parse_describe(output: str) -> ScmVersion: + tag, distance, node, dirty = _git_parse_describe(output) + return meta(tag=tag, distance=distance, dirty=dirty, node=node, config=config) + + return describe_res.parse_success(parse=parse_describe) + + +def _git_parse_inner( + config: Configuration, + wd: GitWorkdir | hg_git.GitWorkdirHgClient, + pre_parse: None | (Callable[[GitWorkdir | hg_git.GitWorkdirHgClient], None]) = None, + describe_command: _t.CMD_TYPE | None = None, +) -> ScmVersion: + if pre_parse: + pre_parse(wd) + + version = version_from_describe(wd, config, describe_command) + + if version is None: + # If 'git git_describe_command' failed, try to get the information otherwise. + tag = config.version_cls("0.0") + node = wd.node() + if node is None: + distance = 0 + dirty = True + else: + distance = wd.count_all_nodes() + node = "g" + node + dirty = wd.is_dirty() + version = meta( + tag=tag, distance=distance, dirty=dirty, node=node, config=config + ) + branch = wd.get_branch() + node_date = wd.get_head_date() or datetime.now(timezone.utc).date() + return dataclasses.replace(version, branch=branch, node_date=node_date) + + +def _git_parse_describe( + describe_output: str, +) -> tuple[str, int, str | None, bool]: + # 'describe_output' looks e.g. like 'v1.5.0-0-g4060507' or + # 'v1.15.1rc1-37-g9bd1298-dirty'. + # It may also just be a bare tag name if this is a tagged commit and we are + # parsing a .git_archival.txt file. + + if describe_output.endswith("-dirty"): + dirty = True + describe_output = describe_output[:-6] + else: + dirty = False + + split = describe_output.rsplit("-", 2) + if len(split) < 3: # probably a tagged commit + tag = describe_output + number = 0 + node = None + else: + tag, number_, node = split + number = int(number_) + return tag, number, node, dirty + + +def archival_to_version( + data: dict[str, str], config: Configuration +) -> ScmVersion | None: + node: str | None + log.debug("data %s", data) + archival_describe = data.get("describe-name", DESCRIBE_UNSUPPORTED) + if DESCRIBE_UNSUPPORTED in archival_describe: + warnings.warn("git archive did not support describe output") + else: + tag, number, node, _ = _git_parse_describe(archival_describe) + return meta( + tag, + config=config, + distance=number, + node=node, + ) + + for ref in REF_TAG_RE.findall(data.get("ref-names", "")): + version = tag_to_version(ref, config) + if version is not None: + return meta(version, config=config) + else: + node = data.get("node") + if node is None: + return None + elif "$FORMAT" in node.upper(): + warnings.warn("unprocessed git archival found (no export subst applied)") + return None + else: + return meta("0.0", node=node, config=config) + + +def parse_archival(root: _t.PathT, config: Configuration) -> ScmVersion | None: + archival = os.path.join(root, ".git_archival.txt") + data = data_from_mime(archival) + return archival_to_version(data, config=config) diff --git a/setuptools/_build_vendor/setuptools_scm/hg.py b/setuptools/_build_vendor/setuptools_scm/hg.py new file mode 100644 index 0000000000..dbffb0aeaa --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/hg.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +import datetime +import logging +import os + +from pathlib import Path +from typing import TYPE_CHECKING + +from . import Configuration +from ._version_cls import Version +from .integration import data_from_mime +from .scm_workdir import Workdir +from .version import ScmVersion +from .version import meta +from .version import tag_to_version + +if TYPE_CHECKING: + from . import _types as _t + +from ._run_cmd import require_command as _require_command +from ._run_cmd import run as _run + +log = logging.getLogger(__name__) + + +class HgWorkdir(Workdir): + @classmethod + def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None: + res = _run(["hg", "root"], wd) + if res.returncode: + return None + return cls(Path(res.stdout)) + + def get_meta(self, config: Configuration) -> ScmVersion | None: + node: str + tags_str: str + node_date_str: str + node, tags_str, node_date_str = self.hg_log( + ".", "{node}\n{tag}\n{date|shortdate}" + ).split("\n") + + # TODO: support bookmarks and topics (but nowadays bookmarks are + # mainly used to emulate Git branches, which is already supported with + # the dedicated class GitWorkdirHgClient) + + branch, dirty_str, dirty_date = _run( + ["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"], + cwd=self.path, + check=True, + ).stdout.split("\n") + dirty = bool(int(dirty_str)) + node_date = datetime.date.fromisoformat(dirty_date if dirty else node_date_str) + + if node == "0" * len(node): + log.debug("initial node %s", self.path) + return meta( + Version("0.0"), + config=config, + dirty=dirty, + branch=branch, + node_date=node_date, + ) + + node = "h" + node[:7] + + tags = tags_str.split() + if "tip" in tags: + # tip is not a real tag + tags.remove("tip") + + if tags: + tag = tag_to_version(tags[0], config) + if tag: + return meta(tag, dirty=dirty, branch=branch, config=config) + + try: + tag_str = self.get_latest_normalizable_tag() + if tag_str is None: + dist = self.get_distance_revs("") + else: + dist = self.get_distance_revs(tag_str) + + if tag_str == "null" or tag_str is None: + tag = Version("0.0") + dist += 1 + else: + tag = tag_to_version(tag_str, config=config) + assert tag is not None + + if self.check_changes_since_tag(tag_str) or dirty: + return meta( + tag, + distance=dist, + node=node, + dirty=dirty, + branch=branch, + config=config, + node_date=node_date, + ) + else: + return meta(tag, config=config, node_date=node_date) + + except ValueError as e: + log.exception("error %s", e) + pass # unpacking failed, old hg + + return None + + def hg_log(self, revset: str, template: str) -> str: + cmd = ["hg", "log", "-r", revset, "-T", template] + + return _run(cmd, cwd=self.path, check=True).stdout + + def get_latest_normalizable_tag(self) -> str | None: + # Gets all tags containing a '.' (see #229) from oldest to newest + outlines = self.hg_log( + revset="ancestors(.) and tag('re:\\.')", + template="{tags}{if(tags, '\n', '')}", + ).split() + if not outlines: + return None + tag = outlines[-1].split()[-1] + return tag + + def get_distance_revs(self, rev1: str, rev2: str = ".") -> int: + revset = f"({rev1}::{rev2})" + out = self.hg_log(revset, ".") + return len(out) - 1 + + def check_changes_since_tag(self, tag: str | None) -> bool: + if tag == "0.0" or tag is None: + return True + + revset = ( + "(branch(.)" # look for revisions in this branch only + f" and tag({tag!r})::." # after the last tag + # ignore commits that only modify .hgtags and nothing else: + " and (merge() or file('re:^(?!\\.hgtags).*$'))" + f" and not tag({tag!r}))" # ignore the tagged commit itself + ) + + return bool(self.hg_log(revset, ".")) + + +def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: + _require_command("hg") + if os.path.exists(os.path.join(root, ".hg/git")): + res = _run(["hg", "path"], root) + if not res.returncode: + for line in res.stdout.split("\n"): + if line.startswith("default ="): + path = Path(line.split()[2]) + if path.name.endswith(".git") or (path / ".git").exists(): + from .git import _git_parse_inner + from .hg_git import GitWorkdirHgClient + + wd_hggit = GitWorkdirHgClient.from_potential_worktree(root) + if wd_hggit: + return _git_parse_inner(config, wd_hggit) + + wd = HgWorkdir.from_potential_worktree(config.absolute_root) + + if wd is None: + return None + + return wd.get_meta(config) + + +def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersion: + log.debug("data %s", data) + node = data.get("node", "")[:12] + if node: + node = "h" + node + if "tag" in data: + return meta(data["tag"], config=config) + elif "latesttag" in data: + return meta( + data["latesttag"], + distance=int(data["latesttagdistance"]), + node=node, + branch=data.get("branch"), + config=config, + ) + else: + return meta(config.version_cls("0.0"), node=node, config=config) + + +def parse_archival(root: _t.PathT, config: Configuration) -> ScmVersion: + archival = os.path.join(root, ".hg_archival.txt") + data = data_from_mime(archival) + return archival_to_version(data, config=config) diff --git a/setuptools/_build_vendor/setuptools_scm/hg_git.py b/setuptools/_build_vendor/setuptools_scm/hg_git.py new file mode 100644 index 0000000000..9cab6f4528 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/hg_git.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import logging +import os + +from contextlib import suppress +from datetime import date +from pathlib import Path + +from . import _types as _t +from ._run_cmd import CompletedProcess as _CompletedProcess +from ._run_cmd import require_command +from ._run_cmd import run as _run +from .git import GitWorkdir +from .hg import HgWorkdir + +log = logging.getLogger(__name__) + +_FAKE_GIT_DESCRIBE_ERROR = _CompletedProcess( + "fake git describe output for hg", + 1, + "<>hg git failed to describe", +) + + +class GitWorkdirHgClient(GitWorkdir, HgWorkdir): + COMMAND = "hg" + + @classmethod + def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None: + require_command("hg") + res = _run(["hg", "root"], cwd=wd).parse_success(parse=Path) + if res is None: + return None + return cls(res) + + def is_dirty(self) -> bool: + res = _run(["hg", "id", "-T", "{dirty}"], cwd=self.path, check=True) + return bool(res.stdout) + + def get_branch(self) -> str | None: + res = _run(["hg", "id", "-T", "{bookmarks}"], cwd=self.path) + if res.returncode: + log.info("branch err %s", res) + return None + return res.stdout + + def get_head_date(self) -> date | None: + return _run('hg log -r . -T "{shortdate(date)}"', cwd=self.path).parse_success( + parse=date.fromisoformat, error_msg="head date err" + ) + + def is_shallow(self) -> bool: + return False + + def fetch_shallow(self) -> None: + pass + + def get_hg_node(self) -> str | None: + res = _run('hg log -r . -T "{node}"', cwd=self.path) + if res.returncode: + return None + else: + return res.stdout + + def _hg2git(self, hg_node: str) -> str | None: + with suppress(FileNotFoundError): + with open(os.path.join(self.path, ".hg/git-mapfile")) as map_items: + for item in map_items: + if hg_node in item: + git_node, hg_node = item.split() + return git_node + return None + + def node(self) -> str | None: + hg_node = self.get_hg_node() + if hg_node is None: + return None + + git_node = self._hg2git(hg_node) + + if git_node is None: + # trying again after hg -> git + _run(["hg", "gexport"], cwd=self.path) + git_node = self._hg2git(hg_node) + + if git_node is None: + log.debug("Cannot get git node so we use hg node %s", hg_node) + + if hg_node == "0" * len(hg_node): + # mimic Git behavior + return None + + return hg_node + + return git_node[:7] + + def count_all_nodes(self) -> int: + res = _run(["hg", "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) + return len(res.stdout) + + def default_describe(self) -> _CompletedProcess: + """ + Tentative to reproduce the output of + + `git describe --dirty --tags --long --match *[0-9]*` + + """ + res = _run( + [ + "hg", + "log", + "-r", + "(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))", + "-T", + "{tags}{if(tags, ' ', '')}", + ], + cwd=self.path, + ) + if res.returncode: + return _FAKE_GIT_DESCRIBE_ERROR + hg_tags: list[str] = res.stdout.split() + + if not hg_tags: + return _FAKE_GIT_DESCRIBE_ERROR + + with self.path.joinpath(".hg/git-tags").open() as fp: + git_tags: dict[str, str] = dict(line.split()[::-1] for line in fp) + + tag: str + for hg_tag in hg_tags: + if hg_tag in git_tags: + tag = hg_tag + break + else: + logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags) + return _FAKE_GIT_DESCRIBE_ERROR + + res = _run(["hg", "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path) + if res.returncode: + return _FAKE_GIT_DESCRIBE_ERROR + distance = len(res.stdout) - 1 + + node = self.node() + assert node is not None + desc = f"{tag}-{distance}-g{node}" + + if self.is_dirty(): + desc += "-dirty" + log.debug("faked describe %r", desc) + return _CompletedProcess( + ["setuptools-scm", "faked", "describe"], + returncode=0, + stdout=desc, + stderr="", + ) diff --git a/setuptools/_build_vendor/setuptools_scm/integration.py b/setuptools/_build_vendor/setuptools_scm/integration.py new file mode 100644 index 0000000000..48874e3847 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/integration.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import logging +import textwrap + +from pathlib import Path + +from . import _types as _t + +log = logging.getLogger(__name__) + + +def data_from_mime(path: _t.PathT, content: None | str = None) -> dict[str, str]: + """return a mapping from mime/pseudo-mime content + :param path: path to the mime file + :param content: content of the mime file, if None, read from path + :rtype: dict[str, str] + + """ + + if content is None: + content = Path(path).read_text(encoding="utf-8") + log.debug("mime %s content:\n%s", path, textwrap.indent(content, " ")) + + from email.parser import HeaderParser + + parser = HeaderParser() + message = parser.parsestr(content) + data = dict(message.items()) + log.debug("mime %s data:\n%s", path, data) + return data diff --git a/setuptools/_build_vendor/setuptools_scm/scm_workdir.py b/setuptools/_build_vendor/setuptools_scm/scm_workdir.py new file mode 100644 index 0000000000..9879549d6d --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/scm_workdir.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +from ._config import Configuration +from .version import ScmVersion + + +@dataclass() +class Workdir: + path: Path + + def run_describe(self, config: Configuration) -> ScmVersion: + raise NotImplementedError(self.run_describe) diff --git a/setuptools/_build_vendor/setuptools_scm/version.py b/setuptools/_build_vendor/setuptools_scm/version.py new file mode 100644 index 0000000000..0a36742339 --- /dev/null +++ b/setuptools/_build_vendor/setuptools_scm/version.py @@ -0,0 +1,454 @@ +from __future__ import annotations + +import dataclasses +import logging +import os +import re +import warnings + +from datetime import date +from datetime import datetime +from datetime import timezone +from typing import TYPE_CHECKING +from typing import Any +from typing import Callable +from typing import Match + +from . import _entrypoints +from . import _modify_version + +if TYPE_CHECKING: + import sys + + if sys.version_info >= (3, 10): + from typing import Concatenate + from typing import ParamSpec + else: + from typing_extensions import Concatenate + from typing_extensions import ParamSpec + + _P = ParamSpec("_P") + +from typing import TypedDict + +from . import _config +from . import _version_cls as _v +from ._version_cls import Version as PkgVersion +from ._version_cls import _VersionT + +log = logging.getLogger(__name__) + + +SEMVER_MINOR = 2 +SEMVER_PATCH = 3 +SEMVER_LEN = 3 + + +class _TagDict(TypedDict): + version: str + prefix: str + suffix: str + + +def _parse_version_tag( + tag: str | object, config: _config.Configuration +) -> _TagDict | None: + match = config.tag_regex.match(str(tag)) + + if match: + key: str | int = 1 if len(match.groups()) == 1 else "version" + full = match.group(0) + log.debug("%r %r %s", tag, config.tag_regex, match) + log.debug( + "key %s data %s, %s, %r", key, match.groupdict(), match.groups(), full + ) + result = _TagDict( + version=match.group(key), + prefix=full[: match.start(key)], + suffix=full[match.end(key) :], + ) + + log.debug("tag %r parsed to %r", tag, result) + assert result["version"] + return result + else: + log.debug("tag %r did not parse", tag) + + return None + + +def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any: + log.debug("ep %r %r", group, callable_or_name) + + if callable(callable_or_name): + return callable_or_name + from ._entrypoints import iter_entry_points + + for ep in iter_entry_points(group, callable_or_name): + log.debug("ep found: %s", ep.name) + return ep.load() + + +def tag_to_version( + tag: _VersionT | str, config: _config.Configuration +) -> _VersionT | None: + """ + take a tag that might be prefixed with a keyword and return only the version part + """ + log.debug("tag %s", tag) + + tag_dict = _parse_version_tag(tag, config) + if tag_dict is None or not tag_dict.get("version", None): + warnings.warn(f"tag {tag!r} no version found") + return None + + version_str = tag_dict["version"] + log.debug("version pre parse %s", version_str) + + if suffix := tag_dict.get("suffix", ""): + warnings.warn(f"tag {tag!r} will be stripped of its suffix {suffix!r}") + + version: _VersionT = config.version_cls(version_str) + log.debug("version=%r", version) + + return version + + +def _source_epoch_or_utc_now() -> datetime: + if "SOURCE_DATE_EPOCH" in os.environ: + date_epoch = int(os.environ["SOURCE_DATE_EPOCH"]) + return datetime.fromtimestamp(date_epoch, timezone.utc) + else: + return datetime.now(timezone.utc) + + +@dataclasses.dataclass +class ScmVersion: + """represents a parsed version from scm""" + + tag: _v.Version | _v.NonNormalizedVersion | str + """the related tag or preformatted version string""" + config: _config.Configuration + """the configuration used to parse the version""" + distance: int = 0 + """the number of commits since the tag""" + node: str | None = None + """the shortened node id""" + dirty: bool = False + """whether the working copy had uncommitted changes""" + preformatted: bool = False + """whether the version string was preformatted""" + branch: str | None = None + """the branch name if any""" + node_date: date | None = None + """the date of the commit if available""" + time: datetime = dataclasses.field(default_factory=_source_epoch_or_utc_now) + """the current time or source epoch time + only set for unit-testing version schemes + for real usage it must be `now(utc)` or `SOURCE_EPOCH` + """ + + @property + def exact(self) -> bool: + """returns true checked out exactly on a tag and no local changes apply""" + return self.distance == 0 and not self.dirty + + def __repr__(self) -> str: + return ( + f"" + ) + + def format_with(self, fmt: str, **kw: object) -> str: + """format a given format string with attributes of this object""" + return fmt.format( + time=self.time, + tag=self.tag, + distance=self.distance, + node=self.node, + dirty=self.dirty, + branch=self.branch, + node_date=self.node_date, + **kw, + ) + + def format_choice(self, clean_format: str, dirty_format: str, **kw: object) -> str: + """given `clean_format` and `dirty_format` + + choose one based on `self.dirty` and format it using `self.format_with`""" + + return self.format_with(dirty_format if self.dirty else clean_format, **kw) + + def format_next_version( + self, + guess_next: Callable[Concatenate[ScmVersion, _P], str], + fmt: str = "{guessed}.dev{distance}", + *k: _P.args, + **kw: _P.kwargs, + ) -> str: + guessed = guess_next(self, *k, **kw) + return self.format_with(fmt, guessed=guessed) + + +def _parse_tag( + tag: _VersionT | str, preformatted: bool, config: _config.Configuration +) -> _VersionT | str: + if preformatted: + return tag + elif not isinstance(tag, config.version_cls): + version = tag_to_version(tag, config) + assert version is not None + return version + else: + return tag + + +def meta( + tag: str | _VersionT, + *, + distance: int = 0, + dirty: bool = False, + node: str | None = None, + preformatted: bool = False, + branch: str | None = None, + config: _config.Configuration, + node_date: date | None = None, +) -> ScmVersion: + parsed_version = _parse_tag(tag, preformatted, config) + log.info("version %s -> %s", tag, parsed_version) + assert parsed_version is not None, "Can't parse version %s" % tag + return ScmVersion( + parsed_version, + distance=distance, + node=node, + dirty=dirty, + preformatted=preformatted, + branch=branch, + config=config, + node_date=node_date, + ) + + +def guess_next_version(tag_version: ScmVersion) -> str: + version = _modify_version.strip_local(str(tag_version.tag)) + return _modify_version._bump_dev(version) or _modify_version._bump_regex(version) + + +def guess_next_dev_version(version: ScmVersion) -> str: + if version.exact: + return version.format_with("{tag}") + else: + return version.format_next_version(guess_next_version) + + +def guess_next_simple_semver( + version: ScmVersion, retain: int, increment: bool = True +) -> str: + if isinstance(version.tag, _v.Version): + parts = list(version.tag.release[:retain]) + else: + try: + parts = [int(i) for i in str(version.tag).split(".")[:retain]] + except ValueError: + raise ValueError(f"{version} can't be parsed as numeric version") from None + while len(parts) < retain: + parts.append(0) + if increment: + parts[-1] += 1 + while len(parts) < SEMVER_LEN: + parts.append(0) + return ".".join(str(i) for i in parts) + + +def simplified_semver_version(version: ScmVersion) -> str: + if version.exact: + return guess_next_simple_semver(version, retain=SEMVER_LEN, increment=False) + else: + if version.branch is not None and "feature" in version.branch: + return version.format_next_version( + guess_next_simple_semver, retain=SEMVER_MINOR + ) + else: + return version.format_next_version( + guess_next_simple_semver, retain=SEMVER_PATCH + ) + + +def release_branch_semver_version(version: ScmVersion) -> str: + if version.exact: + return version.format_with("{tag}") + if version.branch is not None: + # Does the branch name (stripped of namespace) parse as a version? + branch_ver_data = _parse_version_tag( + version.branch.split("/")[-1], version.config + ) + if branch_ver_data is not None: + branch_ver = branch_ver_data["version"] + if branch_ver[0] == "v": + # Allow branches that start with 'v', similar to Version. + branch_ver = branch_ver[1:] + # Does the branch version up to the minor part match the tag? If not it + # might be like, an issue number or something and not a version number, so + # we only want to use it if it matches. + tag_ver_up_to_minor = str(version.tag).split(".")[:SEMVER_MINOR] + branch_ver_up_to_minor = branch_ver.split(".")[:SEMVER_MINOR] + if branch_ver_up_to_minor == tag_ver_up_to_minor: + # We're in a release/maintenance branch, next is a patch/rc/beta bump: + return version.format_next_version(guess_next_version) + # We're in a development branch, next is a minor bump: + return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR) + + +def release_branch_semver(version: ScmVersion) -> str: + warnings.warn( + "release_branch_semver is deprecated and will be removed in the future. " + "Use release_branch_semver_version instead", + category=DeprecationWarning, + stacklevel=2, + ) + return release_branch_semver_version(version) + + +def only_version(version: ScmVersion) -> str: + return version.format_with("{tag}") + + +def no_guess_dev_version(version: ScmVersion) -> str: + if version.exact: + return version.format_with("{tag}") + else: + return version.format_next_version(_modify_version._dont_guess_next_version) + + +_DATE_REGEX = re.compile( + r""" + ^(?P + (?P[vV]?) + (?P\d{2}|\d{4})(?:\.\d{1,2}){2}) + (?:\.(?P\d*))?$ + """, + re.VERBOSE, +) + + +def date_ver_match(ver: str) -> Match[str] | None: + return _DATE_REGEX.match(ver) + + +def guess_next_date_ver( + version: ScmVersion, + node_date: date | None = None, + date_fmt: str | None = None, + version_cls: type | None = None, +) -> str: + """ + same-day -> patch +1 + other-day -> today + + distance is always added as .devX + """ + match = date_ver_match(str(version.tag)) + if match is None: + warnings.warn( + f"{version} does not correspond to a valid versioning date, " + "assuming legacy version" + ) + if date_fmt is None: + date_fmt = "%y.%m.%d" + else: + # deduct date format if not provided + if date_fmt is None: + date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d" + if prefix := match.group("prefix"): + if not date_fmt.startswith(prefix): + date_fmt = prefix + date_fmt + + today = version.time.date() + head_date = node_date or today + # compute patch + if match is None: + tag_date = today + else: + tag_date = ( + datetime.strptime(match.group("date"), date_fmt) + .replace(tzinfo=timezone.utc) + .date() + ) + if tag_date == head_date: + patch = "0" if match is None else (match.group("patch") or "0") + patch = int(patch) + 1 + else: + if tag_date > head_date and match is not None: + # warn on future times + warnings.warn( + f"your previous tag ({tag_date})" + f" is ahead your node date ({head_date})" + ) + patch = 0 + next_version = "{node_date:{date_fmt}}.{patch}".format( + node_date=head_date, date_fmt=date_fmt, patch=patch + ) + # rely on the Version object to ensure consistency (e.g. remove leading 0s) + if version_cls is None: + version_cls = PkgVersion + next_version = str(version_cls(next_version)) + return next_version + + +def calver_by_date(version: ScmVersion) -> str: + if version.exact and not version.dirty: + return version.format_with("{tag}") + # TODO: move the release-X check to a new scheme + if version.branch is not None and version.branch.startswith("release-"): + branch_ver = _parse_version_tag(version.branch.split("-")[-1], version.config) + if branch_ver is not None: + ver = branch_ver["version"] + match = date_ver_match(ver) + if match: + return ver + return version.format_next_version( + guess_next_date_ver, + node_date=version.node_date, + version_cls=version.config.version_cls, + ) + + +def get_local_node_and_date(version: ScmVersion) -> str: + return _modify_version._format_local_with_time(version, time_format="%Y%m%d") + + +def get_local_node_and_timestamp(version: ScmVersion) -> str: + return _modify_version._format_local_with_time(version, time_format="%Y%m%d%H%M%S") + + +def get_local_dirty_tag(version: ScmVersion) -> str: + return version.format_choice("", "+dirty") + + +def get_no_local_node(version: ScmVersion) -> str: + return "" + + +def postrelease_version(version: ScmVersion) -> str: + if version.exact: + return version.format_with("{tag}") + else: + return version.format_with("{tag}.post{distance}") + + +def format_version(version: ScmVersion) -> str: + log.debug("scm version %s", version) + log.debug("config %s", version.config) + if version.preformatted: + assert isinstance(version.tag, str) + return version.tag + main_version = _entrypoints._call_version_scheme( + version, "setuptools_scm.version_scheme", version.config.version_scheme, None + ) + log.debug("version %s", main_version) + assert main_version is not None + local_version = _entrypoints._call_version_scheme( + version, "setuptools_scm.local_scheme", version.config.local_scheme, "+unknown" + ) + log.debug("local_version %s", local_version) + return main_version + local_version diff --git a/tools/finalize.py b/tools/finalize.py deleted file mode 100644 index 3ba5d16ac7..0000000000 --- a/tools/finalize.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Finalize the repo for a release. Invokes towncrier and bumpversion. -""" - -__requires__ = ['bump2version', 'towncrier', 'jaraco.develop>=7.21'] - - -import subprocess -import pathlib -import re -import sys - -from jaraco.develop import towncrier - - -bump_version_command = [ - sys.executable, - '-m', - 'bumpversion', - towncrier.release_kind(), -] - - -def get_version(): - cmd = bump_version_command + ['--dry-run', '--verbose'] - out = subprocess.check_output(cmd, text=True, encoding='utf-8') - return re.search('^new_version=(.*)', out, re.MULTILINE).group(1) - - -def update_changelog(): - towncrier.run('build', '--yes') - _repair_changelog() - - -def _repair_changelog(): - """ - Workaround for #2666 - """ - changelog_fn = pathlib.Path('NEWS.rst') - changelog = changelog_fn.read_text(encoding='utf-8') - fixed = re.sub(r'^(v[0-9.]+)v[0-9.]+$', r'\1', changelog, flags=re.M) - changelog_fn.write_text(fixed, encoding='utf-8') - subprocess.check_output(['git', 'add', changelog_fn]) - - -def bump_version(): - cmd = bump_version_command + ['--allow-dirty'] - subprocess.check_call(cmd) - - -def ensure_config(): - """ - Double-check that Git has an e-mail configured. - """ - subprocess.check_output(['git', 'config', 'user.email']) - - -if __name__ == '__main__': - print("Cutting release at", get_version()) - ensure_config() - towncrier.check_changes() - update_changelog() - bump_version() diff --git a/tox.ini b/tox.ini index 3bd33bdf27..147dbbec84 100644 --- a/tox.ini +++ b/tox.ini @@ -63,12 +63,10 @@ description = assemble changelog and tag a release skip_install = True deps = towncrier - bump2version jaraco.develop >= 7.23 - importlib_resources < 6 # twisted/towncrier#528 (waiting for release) pass_env = * commands = - python tools/finalize.py + python -m jaraco.develop.finalize [testenv:vendor] skip_install = True