From 353f920b7a1a95c28ba1234d275324c351d6f4a3 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 23 Nov 2021 11:15:03 +0000 Subject: [PATCH 01/14] Add release note for 3.5.1 --- doc/history.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/history.rst b/doc/history.rst index 53efaa2a..e3aa6e44 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -1,6 +1,13 @@ Release history =============== +Version 3.5.1 +------------- + +- Fix development installs with ``flit install --symlink`` and ``--pth-file``, + which were broken in 3.5.0, especially for packages using a ``src`` folder + (:ghpull:`472`). + Version 3.5 ----------- From e384a191c06e2f2aecf274945ad3442a25e99a28 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 1 Dec 2021 13:37:57 -0800 Subject: [PATCH 02/14] Replace /takluyver/ by /pypa/ where relevant While GitHub does redirect, it is good practice to fix links. And the redirect will last only until Thomas decide to fork it again. --- doc/cmdline.rst | 2 +- doc/conf.py | 2 +- doc/development.rst | 2 +- doc/flit_ini.rst | 4 ++-- doc/pyproject_toml.rst | 4 ++-- flit/__init__.py | 2 +- flit_core/flit_core/build_thyself.py | 19 ++++++++++--------- flit_core/flit_core/tests/samples/module2.py | 2 +- flit_core/flit_core/tests/test_wheel.py | 2 +- pyproject.toml | 2 +- tests/test_validate.py | 7 ++++--- 11 files changed, 25 insertions(+), 23 deletions(-) diff --git a/doc/cmdline.rst b/doc/cmdline.rst index d0960a75..78da16d5 100644 --- a/doc/cmdline.rst +++ b/doc/cmdline.rst @@ -219,7 +219,7 @@ Environment variables If the metadata is invalid, uploading the package to PyPI may fail. This environment variable provides an escape hatch in case Flit incorrectly rejects your valid metadata. If you need to use it and you believe your - metadata is valid, please `open an issue `__. + metadata is valid, please `open an issue `__. .. envvar:: FLIT_INSTALL_PYTHON diff --git a/doc/conf.py b/doc/conf.py index 77526e12..161841d5 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -34,7 +34,7 @@ 'sphinx_rtd_theme', ] -github_project_url = "https://github.com/takluyver/flit" +github_project_url = "https://github.com/pypa/flit" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/doc/development.rst b/doc/development.rst index f714999e..f0d7b216 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -3,7 +3,7 @@ Developing Flit To get a development installation of Flit itself:: - git clone https://github.com/takluyver/flit.git + git clone https://github.com/pypa/flit.git cd flit python3 -m pip install docutils requests toml python3 bootstrap_dev.py diff --git a/doc/flit_ini.rst b/doc/flit_ini.rst index f264b328..b41d967c 100644 --- a/doc/flit_ini.rst +++ b/doc/flit_ini.rst @@ -33,7 +33,7 @@ e.g. for flit itself module=flit author=Thomas Kluyver author-email=thomas@kluyver.me.uk - home-page=https://github.com/takluyver/flit + home-page=https://github.com/pypa/flit The remaining fields are optional: @@ -86,7 +86,7 @@ Here's the full example from flit itself: [metadata] author=Thomas Kluyver author-email=thomas@kluyver.me.uk - home-page=https://github.com/takluyver/flit + home-page=https://github.com/pypa/flit requires=requests requires-python= >=3 description-file=README.rst diff --git a/doc/pyproject_toml.rst b/doc/pyproject_toml.rst index acb2c030..6ef305ab 100644 --- a/doc/pyproject_toml.rst +++ b/doc/pyproject_toml.rst @@ -165,7 +165,7 @@ any names inside it. Here it is for flit: [project.urls] Documentation = "https://flit.readthedocs.io/en/latest/" - Source = "https://github.com/takluyver/flit" + Source = "https://github.com/pypa/flit" .. _pyproject_project_scripts: @@ -332,7 +332,7 @@ Here was the metadata section from flit using the older style: module="flit" author="Thomas Kluyver" author-email="thomas@kluyver.me.uk" - home-page="https://github.com/takluyver/flit" + home-page="https://github.com/pypa/flit" requires=[ "flit_core >=2.2.0", "requests", diff --git a/flit/__init__.py b/flit/__init__.py index f0b18165..4700a6ed 100644 --- a/flit/__init__.py +++ b/flit/__init__.py @@ -30,7 +30,7 @@ def find_python_executable(python: Optional[str] = None) -> str: return python # get absolute filepath of {python} # shutil.which may give a different result to the raw subprocess call - # see https://github.com/takluyver/flit/pull/300 and https://bugs.python.org/issue38905 + # see https://github.com/pypa/flit/pull/300 and https://bugs.python.org/issue38905 resolved_python = shutil.which(python) if resolved_python is None: raise PythonNotFoundError("Unable to resolve Python executable {!r}".format(python)) diff --git a/flit_core/flit_core/build_thyself.py b/flit_core/flit_core/build_thyself.py index f14e33ca..2688e842 100644 --- a/flit_core/flit_core/build_thyself.py +++ b/flit_core/flit_core/build_thyself.py @@ -17,15 +17,16 @@ from . import __version__ metadata_dict = { - 'name': 'flit_core', - 'version': __version__, - 'author': 'Thomas Kluyver & contributors', - 'author_email': 'thomas@kluyver.me.uk', - 'home_page': 'https://github.com/takluyver/flit', - 'summary': ('Distribution-building parts of Flit. ' - 'See flit package for more information'), - 'requires_dist': [ - 'tomli', + "name": "flit_core", + "version": __version__, + "author": "Thomas Kluyver & contributors", + "author_email": "thomas@kluyver.me.uk", + "home_page": "https://github.com/pypa/flit", + "summary": ( + "Distribution-building parts of Flit. " "See flit package for more information" + ), + "requires_dist": [ + "tomli", ], 'requires_python': '>=3.6', 'classifiers': [ diff --git a/flit_core/flit_core/tests/samples/module2.py b/flit_core/flit_core/tests/samples/module2.py index 70c868b3..0f36679f 100644 --- a/flit_core/flit_core/tests/samples/module2.py +++ b/flit_core/flit_core/tests/samples/module2.py @@ -4,7 +4,7 @@ a = {} # An assignment to a subscript (a['test']) broke introspection -# https://github.com/takluyver/flit/issues/343 +# https://github.com/pypa/flit/issues/343 a['test'] = 6 __version__ = '7.0' diff --git a/flit_core/flit_core/tests/test_wheel.py b/flit_core/flit_core/tests/test_wheel.py index b1e4a16f..9bb38fb2 100644 --- a/flit_core/flit_core/tests/test_wheel.py +++ b/flit_core/flit_core/tests/test_wheel.py @@ -8,7 +8,7 @@ samples_dir = Path(__file__).parent / 'samples' def test_licenses_dir(tmp_path): - # Smoketest for https://github.com/takluyver/flit/issues/399 + # Smoketest for https://github.com/pypa/flit/issues/399 info = make_wheel_in(samples_dir / 'inclusion' / 'pyproject.toml', tmp_path) assert_isfile(info.file) diff --git a/pyproject.toml b/pyproject.toml index 9a4e8b17..fe6d3432 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ doc = [ [project.urls] Documentation = "https://flit.readthedocs.io/en/latest/" -Source = "https://github.com/takluyver/flit" +Source = "https://github.com/pypa/flit" [project.scripts] flit = "flit:main" diff --git a/tests/test_validate.py b/tests/test_validate.py index 2c2da5fe..21b918cb 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -90,10 +90,11 @@ def test_validate_environment_marker(): def test_validate_url(): vurl = fv.validate_url - assert vurl('https://github.com/takluyver/flit') == [] + assert vurl("https://github.com/pypa/flit") == [] + + assert len(vurl("github.com/pypa/flit")) == 1 + assert len(vurl("https://")) == 1 - assert len(vurl('github.com/takluyver/flit')) == 1 - assert len(vurl('https://')) == 1 def test_validate_project_urls(): vpu = fv.validate_project_urls From 49d3cee0fa52b304f285855b370b61dff9df642e Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Tue, 7 Dec 2021 13:50:38 -0600 Subject: [PATCH 03/14] flit-core: add comment explaining where deps are listed --- flit_core/pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flit_core/pyproject.toml b/flit_core/pyproject.toml index 9834f379..ed47c60a 100644 --- a/flit_core/pyproject.toml +++ b/flit_core/pyproject.toml @@ -1,4 +1,6 @@ [build-system] +# Dependencies listed in flit_core/build_thyself.py +# https://github.com/pypa/flit/issues/482#issuecomment-987769343 requires = [] build-backend = "flit_core.build_thyself" backend-path = ["."] From 78b5284153e3075b8d8c317ee3ef62d88ca6a773 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 8 Dec 2021 09:51:56 +0000 Subject: [PATCH 04/14] Clarify comment in pyproject.toml --- flit_core/pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flit_core/pyproject.toml b/flit_core/pyproject.toml index ed47c60a..a1ca5a37 100644 --- a/flit_core/pyproject.toml +++ b/flit_core/pyproject.toml @@ -1,6 +1,7 @@ [build-system] -# Dependencies listed in flit_core/build_thyself.py -# https://github.com/pypa/flit/issues/482#issuecomment-987769343 requires = [] build-backend = "flit_core.build_thyself" backend-path = ["."] + +# Runtime dependencies & other metadata are listed in flit_core/build_thyself.py +# https://github.com/pypa/flit/issues/482#issuecomment-987769343 From dad3ab91331d4b6ff0d613f39acb1452f266e3c2 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 22 Dec 2021 23:46:12 +0000 Subject: [PATCH 05/14] Add script to vendor tomli in flit_core --- flit_core/flit_core/vendor/__init__.py | 0 flit_core/update-vendored-tomli.sh | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 flit_core/flit_core/vendor/__init__.py create mode 100755 flit_core/update-vendored-tomli.sh diff --git a/flit_core/flit_core/vendor/__init__.py b/flit_core/flit_core/vendor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/flit_core/update-vendored-tomli.sh b/flit_core/update-vendored-tomli.sh new file mode 100755 index 00000000..c10af1fa --- /dev/null +++ b/flit_core/update-vendored-tomli.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Update the vendored copy of tomli +set -euo pipefail + +version=$1 +echo "Bundling tomli version $version" + +rm -rf flit_core/vendor/tomli* +pip install --target flit_core/vendor/ "tomli==$version" + +# Convert absolute imports to relative (from tomli.foo -> from .foo) +for file in flit_core/vendor/tomli/*.py; do + sed -i -E 's/((from|import)[[:space:]]+)tomli\./\1\./' "$file" +done + +# Delete some files that aren't useful in this context. +# Leave LICENSE & METADATA present. +rm flit_core/vendor/tomli*.dist-info/{INSTALLER,RECORD,REQUESTED,WHEEL} From de383449df2c1a14f9b09b5dafd6af736dd6a001 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 23 Dec 2021 10:49:20 +0000 Subject: [PATCH 06/14] Vendor tomli 1.2.3 --- .../vendor/tomli-1.2.3.dist-info/LICENSE | 21 + .../vendor/tomli-1.2.3.dist-info/METADATA | 208 ++++++ flit_core/flit_core/vendor/tomli/__init__.py | 9 + flit_core/flit_core/vendor/tomli/_parser.py | 663 ++++++++++++++++++ flit_core/flit_core/vendor/tomli/_re.py | 101 +++ flit_core/flit_core/vendor/tomli/_types.py | 6 + flit_core/flit_core/vendor/tomli/py.typed | 1 + 7 files changed, 1009 insertions(+) create mode 100644 flit_core/flit_core/vendor/tomli-1.2.3.dist-info/LICENSE create mode 100644 flit_core/flit_core/vendor/tomli-1.2.3.dist-info/METADATA create mode 100644 flit_core/flit_core/vendor/tomli/__init__.py create mode 100644 flit_core/flit_core/vendor/tomli/_parser.py create mode 100644 flit_core/flit_core/vendor/tomli/_re.py create mode 100644 flit_core/flit_core/vendor/tomli/_types.py create mode 100644 flit_core/flit_core/vendor/tomli/py.typed diff --git a/flit_core/flit_core/vendor/tomli-1.2.3.dist-info/LICENSE b/flit_core/flit_core/vendor/tomli-1.2.3.dist-info/LICENSE new file mode 100644 index 00000000..e859590f --- /dev/null +++ b/flit_core/flit_core/vendor/tomli-1.2.3.dist-info/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Taneli Hukkinen + +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/flit_core/flit_core/vendor/tomli-1.2.3.dist-info/METADATA b/flit_core/flit_core/vendor/tomli-1.2.3.dist-info/METADATA new file mode 100644 index 00000000..0ddc5864 --- /dev/null +++ b/flit_core/flit_core/vendor/tomli-1.2.3.dist-info/METADATA @@ -0,0 +1,208 @@ +Metadata-Version: 2.1 +Name: tomli +Version: 1.2.3 +Summary: A lil' TOML parser +Keywords: toml +Author-email: Taneli Hukkinen +Requires-Python: >=3.6 +Description-Content-Type: text/markdown +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: MacOS +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Typing :: Typed +Project-URL: Changelog, https://github.com/hukkin/tomli/blob/master/CHANGELOG.md +Project-URL: Homepage, https://github.com/hukkin/tomli + +[![Build Status](https://github.com/hukkin/tomli/workflows/Tests/badge.svg?branch=master)](https://github.com/hukkin/tomli/actions?query=workflow%3ATests+branch%3Amaster+event%3Apush) +[![codecov.io](https://codecov.io/gh/hukkin/tomli/branch/master/graph/badge.svg)](https://codecov.io/gh/hukkin/tomli) +[![PyPI version](https://img.shields.io/pypi/v/tomli)](https://pypi.org/project/tomli) + +# Tomli + +> A lil' TOML parser + +**Table of Contents** *generated with [mdformat-toc](https://github.com/hukkin/mdformat-toc)* + + + +- [Intro](#intro) +- [Installation](#installation) +- [Usage](#usage) + - [Parse a TOML string](#parse-a-toml-string) + - [Parse a TOML file](#parse-a-toml-file) + - [Handle invalid TOML](#handle-invalid-toml) + - [Construct `decimal.Decimal`s from TOML floats](#construct-decimaldecimals-from-toml-floats) +- [FAQ](#faq) + - [Why this parser?](#why-this-parser) + - [Is comment preserving round-trip parsing supported?](#is-comment-preserving-round-trip-parsing-supported) + - [Is there a `dumps`, `write` or `encode` function?](#is-there-a-dumps-write-or-encode-function) + - [How do TOML types map into Python types?](#how-do-toml-types-map-into-python-types) +- [Performance](#performance) + + + +## Intro + +Tomli is a Python library for parsing [TOML](https://toml.io). +Tomli is fully compatible with [TOML v1.0.0](https://toml.io/en/v1.0.0). + +## Installation + +```bash +pip install tomli +``` + +## Usage + +### Parse a TOML string + +```python +import tomli + +toml_str = """ + gretzky = 99 + + [kurri] + jari = 17 + """ + +toml_dict = tomli.loads(toml_str) +assert toml_dict == {"gretzky": 99, "kurri": {"jari": 17}} +``` + +### Parse a TOML file + +```python +import tomli + +with open("path_to_file/conf.toml", "rb") as f: + toml_dict = tomli.load(f) +``` + +The file must be opened in binary mode (with the `"rb"` flag). +Binary mode will enforce decoding the file as UTF-8 with universal newlines disabled, +both of which are required to correctly parse TOML. +Support for text file objects is deprecated for removal in the next major release. + +### Handle invalid TOML + +```python +import tomli + +try: + toml_dict = tomli.loads("]] this is invalid TOML [[") +except tomli.TOMLDecodeError: + print("Yep, definitely not valid.") +``` + +Note that while the `TOMLDecodeError` type is public API, error messages of raised instances of it are not. +Error messages should not be assumed to stay constant across Tomli versions. + +### Construct `decimal.Decimal`s from TOML floats + +```python +from decimal import Decimal +import tomli + +toml_dict = tomli.loads("precision-matters = 0.982492", parse_float=Decimal) +assert toml_dict["precision-matters"] == Decimal("0.982492") +``` + +Note that `decimal.Decimal` can be replaced with another callable that converts a TOML float from string to a Python type. +The `decimal.Decimal` is, however, a practical choice for use cases where float inaccuracies can not be tolerated. + +Illegal types include `dict`, `list`, and anything that has the `append` attribute. +Parsing floats into an illegal type results in undefined behavior. + +## FAQ + +### Why this parser? + +- it's lil' +- pure Python with zero dependencies +- the fastest pure Python parser [\*](#performance): + 15x as fast as [tomlkit](https://pypi.org/project/tomlkit/), + 2.4x as fast as [toml](https://pypi.org/project/toml/) +- outputs [basic data types](#how-do-toml-types-map-into-python-types) only +- 100% spec compliant: passes all tests in + [a test set](https://github.com/toml-lang/compliance/pull/8) + soon to be merged to the official + [compliance tests for TOML](https://github.com/toml-lang/compliance) + repository +- thoroughly tested: 100% branch coverage + +### Is comment preserving round-trip parsing supported? + +No. + +The `tomli.loads` function returns a plain `dict` that is populated with builtin types and types from the standard library only. +Preserving comments requires a custom type to be returned so will not be supported, +at least not by the `tomli.loads` and `tomli.load` functions. + +Look into [TOML Kit](https://github.com/sdispater/tomlkit) if preservation of style is what you need. + +### Is there a `dumps`, `write` or `encode` function? + +[Tomli-W](https://github.com/hukkin/tomli-w) is the write-only counterpart of Tomli, providing `dump` and `dumps` functions. + +The core library does not include write capability, as most TOML use cases are read-only, and Tomli intends to be minimal. + +### How do TOML types map into Python types? + +| TOML type | Python type | Details | +| ---------------- | ------------------- | ------------------------------------------------------------ | +| Document Root | `dict` | | +| Key | `str` | | +| String | `str` | | +| Integer | `int` | | +| Float | `float` | | +| Boolean | `bool` | | +| Offset Date-Time | `datetime.datetime` | `tzinfo` attribute set to an instance of `datetime.timezone` | +| Local Date-Time | `datetime.datetime` | `tzinfo` attribute set to `None` | +| Local Date | `datetime.date` | | +| Local Time | `datetime.time` | | +| Array | `list` | | +| Table | `dict` | | +| Inline Table | `dict` | | + +## Performance + +The `benchmark/` folder in this repository contains a performance benchmark for comparing the various Python TOML parsers. +The benchmark can be run with `tox -e benchmark-pypi`. +Running the benchmark on my personal computer output the following: + +```console +foo@bar:~/dev/tomli$ tox -e benchmark-pypi +benchmark-pypi installed: attrs==19.3.0,click==7.1.2,pytomlpp==1.0.2,qtoml==0.3.0,rtoml==0.7.0,toml==0.10.2,tomli==1.1.0,tomlkit==0.7.2 +benchmark-pypi run-test-pre: PYTHONHASHSEED='2658546909' +benchmark-pypi run-test: commands[0] | python -c 'import datetime; print(datetime.date.today())' +2021-07-23 +benchmark-pypi run-test: commands[1] | python --version +Python 3.8.10 +benchmark-pypi run-test: commands[2] | python benchmark/run.py +Parsing data.toml 5000 times: +------------------------------------------------------ + parser | exec time | performance (more is better) +-----------+------------+----------------------------- + rtoml | 0.901 s | baseline (100%) + pytomlpp | 1.08 s | 83.15% + tomli | 3.89 s | 23.15% + toml | 9.36 s | 9.63% + qtoml | 11.5 s | 7.82% + tomlkit | 56.8 s | 1.59% +``` + +The parsers are ordered from fastest to slowest, using the fastest parser as baseline. +Tomli performed the best out of all pure Python TOML parsers, +losing only to pytomlpp (wraps C++) and rtoml (wraps Rust). + diff --git a/flit_core/flit_core/vendor/tomli/__init__.py b/flit_core/flit_core/vendor/tomli/__init__.py new file mode 100644 index 00000000..85974670 --- /dev/null +++ b/flit_core/flit_core/vendor/tomli/__init__.py @@ -0,0 +1,9 @@ +"""A lil' TOML parser.""" + +__all__ = ("loads", "load", "TOMLDecodeError") +__version__ = "1.2.3" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT + +from ._parser import TOMLDecodeError, load, loads + +# Pretend this exception was created here. +TOMLDecodeError.__module__ = "tomli" diff --git a/flit_core/flit_core/vendor/tomli/_parser.py b/flit_core/flit_core/vendor/tomli/_parser.py new file mode 100644 index 00000000..093afe50 --- /dev/null +++ b/flit_core/flit_core/vendor/tomli/_parser.py @@ -0,0 +1,663 @@ +import string +from types import MappingProxyType +from typing import Any, BinaryIO, Dict, FrozenSet, Iterable, NamedTuple, Optional, Tuple +import warnings + +from ._re import ( + RE_DATETIME, + RE_LOCALTIME, + RE_NUMBER, + match_to_datetime, + match_to_localtime, + match_to_number, +) +from ._types import Key, ParseFloat, Pos + +ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) + +# Neither of these sets include quotation mark or backslash. They are +# currently handled as separate cases in the parser functions. +ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t") +ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n") + +ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS +ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS + +ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS + +TOML_WS = frozenset(" \t") +TOML_WS_AND_NEWLINE = TOML_WS | frozenset("\n") +BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_") +KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'") +HEXDIGIT_CHARS = frozenset(string.hexdigits) + +BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType( + { + "\\b": "\u0008", # backspace + "\\t": "\u0009", # tab + "\\n": "\u000A", # linefeed + "\\f": "\u000C", # form feed + "\\r": "\u000D", # carriage return + '\\"': "\u0022", # quote + "\\\\": "\u005C", # backslash + } +) + + +class TOMLDecodeError(ValueError): + """An error raised if a document is not valid TOML.""" + + +def load(fp: BinaryIO, *, parse_float: ParseFloat = float) -> Dict[str, Any]: + """Parse TOML from a binary file object.""" + s_bytes = fp.read() + try: + s = s_bytes.decode() + except AttributeError: + warnings.warn( + "Text file object support is deprecated in favor of binary file objects." + ' Use `open("foo.toml", "rb")` to open the file in binary mode.', + DeprecationWarning, + stacklevel=2, + ) + s = s_bytes # type: ignore[assignment] + return loads(s, parse_float=parse_float) + + +def loads(s: str, *, parse_float: ParseFloat = float) -> Dict[str, Any]: # noqa: C901 + """Parse TOML from a string.""" + + # The spec allows converting "\r\n" to "\n", even in string + # literals. Let's do so to simplify parsing. + src = s.replace("\r\n", "\n") + pos = 0 + out = Output(NestedDict(), Flags()) + header: Key = () + + # Parse one statement at a time + # (typically means one line in TOML source) + while True: + # 1. Skip line leading whitespace + pos = skip_chars(src, pos, TOML_WS) + + # 2. Parse rules. Expect one of the following: + # - end of file + # - end of line + # - comment + # - key/value pair + # - append dict to list (and move to its namespace) + # - create dict (and move to its namespace) + # Skip trailing whitespace when applicable. + try: + char = src[pos] + except IndexError: + break + if char == "\n": + pos += 1 + continue + if char in KEY_INITIAL_CHARS: + pos = key_value_rule(src, pos, out, header, parse_float) + pos = skip_chars(src, pos, TOML_WS) + elif char == "[": + try: + second_char: Optional[str] = src[pos + 1] + except IndexError: + second_char = None + if second_char == "[": + pos, header = create_list_rule(src, pos, out) + else: + pos, header = create_dict_rule(src, pos, out) + pos = skip_chars(src, pos, TOML_WS) + elif char != "#": + raise suffixed_err(src, pos, "Invalid statement") + + # 3. Skip comment + pos = skip_comment(src, pos) + + # 4. Expect end of line or end of file + try: + char = src[pos] + except IndexError: + break + if char != "\n": + raise suffixed_err( + src, pos, "Expected newline or end of document after a statement" + ) + pos += 1 + + return out.data.dict + + +class Flags: + """Flags that map to parsed keys/namespaces.""" + + # Marks an immutable namespace (inline array or inline table). + FROZEN = 0 + # Marks a nest that has been explicitly created and can no longer + # be opened using the "[table]" syntax. + EXPLICIT_NEST = 1 + + def __init__(self) -> None: + self._flags: Dict[str, dict] = {} + + def unset_all(self, key: Key) -> None: + cont = self._flags + for k in key[:-1]: + if k not in cont: + return + cont = cont[k]["nested"] + cont.pop(key[-1], None) + + def set_for_relative_key(self, head_key: Key, rel_key: Key, flag: int) -> None: + cont = self._flags + for k in head_key: + if k not in cont: + cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}} + cont = cont[k]["nested"] + for k in rel_key: + if k in cont: + cont[k]["flags"].add(flag) + else: + cont[k] = {"flags": {flag}, "recursive_flags": set(), "nested": {}} + cont = cont[k]["nested"] + + def set(self, key: Key, flag: int, *, recursive: bool) -> None: # noqa: A003 + cont = self._flags + key_parent, key_stem = key[:-1], key[-1] + for k in key_parent: + if k not in cont: + cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}} + cont = cont[k]["nested"] + if key_stem not in cont: + cont[key_stem] = {"flags": set(), "recursive_flags": set(), "nested": {}} + cont[key_stem]["recursive_flags" if recursive else "flags"].add(flag) + + def is_(self, key: Key, flag: int) -> bool: + if not key: + return False # document root has no flags + cont = self._flags + for k in key[:-1]: + if k not in cont: + return False + inner_cont = cont[k] + if flag in inner_cont["recursive_flags"]: + return True + cont = inner_cont["nested"] + key_stem = key[-1] + if key_stem in cont: + cont = cont[key_stem] + return flag in cont["flags"] or flag in cont["recursive_flags"] + return False + + +class NestedDict: + def __init__(self) -> None: + # The parsed content of the TOML document + self.dict: Dict[str, Any] = {} + + def get_or_create_nest( + self, + key: Key, + *, + access_lists: bool = True, + ) -> dict: + cont: Any = self.dict + for k in key: + if k not in cont: + cont[k] = {} + cont = cont[k] + if access_lists and isinstance(cont, list): + cont = cont[-1] + if not isinstance(cont, dict): + raise KeyError("There is no nest behind this key") + return cont + + def append_nest_to_list(self, key: Key) -> None: + cont = self.get_or_create_nest(key[:-1]) + last_key = key[-1] + if last_key in cont: + list_ = cont[last_key] + try: + list_.append({}) + except AttributeError: + raise KeyError("An object other than list found behind this key") + else: + cont[last_key] = [{}] + + +class Output(NamedTuple): + data: NestedDict + flags: Flags + + +def skip_chars(src: str, pos: Pos, chars: Iterable[str]) -> Pos: + try: + while src[pos] in chars: + pos += 1 + except IndexError: + pass + return pos + + +def skip_until( + src: str, + pos: Pos, + expect: str, + *, + error_on: FrozenSet[str], + error_on_eof: bool, +) -> Pos: + try: + new_pos = src.index(expect, pos) + except ValueError: + new_pos = len(src) + if error_on_eof: + raise suffixed_err(src, new_pos, f"Expected {expect!r}") from None + + if not error_on.isdisjoint(src[pos:new_pos]): + while src[pos] not in error_on: + pos += 1 + raise suffixed_err(src, pos, f"Found invalid character {src[pos]!r}") + return new_pos + + +def skip_comment(src: str, pos: Pos) -> Pos: + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + if char == "#": + return skip_until( + src, pos + 1, "\n", error_on=ILLEGAL_COMMENT_CHARS, error_on_eof=False + ) + return pos + + +def skip_comments_and_array_ws(src: str, pos: Pos) -> Pos: + while True: + pos_before_skip = pos + pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE) + pos = skip_comment(src, pos) + if pos == pos_before_skip: + return pos + + +def create_dict_rule(src: str, pos: Pos, out: Output) -> Tuple[Pos, Key]: + pos += 1 # Skip "[" + pos = skip_chars(src, pos, TOML_WS) + pos, key = parse_key(src, pos) + + if out.flags.is_(key, Flags.EXPLICIT_NEST) or out.flags.is_(key, Flags.FROZEN): + raise suffixed_err(src, pos, f"Can not declare {key} twice") + out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False) + try: + out.data.get_or_create_nest(key) + except KeyError: + raise suffixed_err(src, pos, "Can not overwrite a value") from None + + if not src.startswith("]", pos): + raise suffixed_err(src, pos, 'Expected "]" at the end of a table declaration') + return pos + 1, key + + +def create_list_rule(src: str, pos: Pos, out: Output) -> Tuple[Pos, Key]: + pos += 2 # Skip "[[" + pos = skip_chars(src, pos, TOML_WS) + pos, key = parse_key(src, pos) + + if out.flags.is_(key, Flags.FROZEN): + raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}") + # Free the namespace now that it points to another empty list item... + out.flags.unset_all(key) + # ...but this key precisely is still prohibited from table declaration + out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False) + try: + out.data.append_nest_to_list(key) + except KeyError: + raise suffixed_err(src, pos, "Can not overwrite a value") from None + + if not src.startswith("]]", pos): + raise suffixed_err(src, pos, 'Expected "]]" at the end of an array declaration') + return pos + 2, key + + +def key_value_rule( + src: str, pos: Pos, out: Output, header: Key, parse_float: ParseFloat +) -> Pos: + pos, key, value = parse_key_value_pair(src, pos, parse_float) + key_parent, key_stem = key[:-1], key[-1] + abs_key_parent = header + key_parent + + if out.flags.is_(abs_key_parent, Flags.FROZEN): + raise suffixed_err( + src, pos, f"Can not mutate immutable namespace {abs_key_parent}" + ) + # Containers in the relative path can't be opened with the table syntax after this + out.flags.set_for_relative_key(header, key, Flags.EXPLICIT_NEST) + try: + nest = out.data.get_or_create_nest(abs_key_parent) + except KeyError: + raise suffixed_err(src, pos, "Can not overwrite a value") from None + if key_stem in nest: + raise suffixed_err(src, pos, "Can not overwrite a value") + # Mark inline table and array namespaces recursively immutable + if isinstance(value, (dict, list)): + out.flags.set(header + key, Flags.FROZEN, recursive=True) + nest[key_stem] = value + return pos + + +def parse_key_value_pair( + src: str, pos: Pos, parse_float: ParseFloat +) -> Tuple[Pos, Key, Any]: + pos, key = parse_key(src, pos) + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + if char != "=": + raise suffixed_err(src, pos, 'Expected "=" after a key in a key/value pair') + pos += 1 + pos = skip_chars(src, pos, TOML_WS) + pos, value = parse_value(src, pos, parse_float) + return pos, key, value + + +def parse_key(src: str, pos: Pos) -> Tuple[Pos, Key]: + pos, key_part = parse_key_part(src, pos) + key: Key = (key_part,) + pos = skip_chars(src, pos, TOML_WS) + while True: + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + if char != ".": + return pos, key + pos += 1 + pos = skip_chars(src, pos, TOML_WS) + pos, key_part = parse_key_part(src, pos) + key += (key_part,) + pos = skip_chars(src, pos, TOML_WS) + + +def parse_key_part(src: str, pos: Pos) -> Tuple[Pos, str]: + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + if char in BARE_KEY_CHARS: + start_pos = pos + pos = skip_chars(src, pos, BARE_KEY_CHARS) + return pos, src[start_pos:pos] + if char == "'": + return parse_literal_str(src, pos) + if char == '"': + return parse_one_line_basic_str(src, pos) + raise suffixed_err(src, pos, "Invalid initial character for a key part") + + +def parse_one_line_basic_str(src: str, pos: Pos) -> Tuple[Pos, str]: + pos += 1 + return parse_basic_str(src, pos, multiline=False) + + +def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, list]: + pos += 1 + array: list = [] + + pos = skip_comments_and_array_ws(src, pos) + if src.startswith("]", pos): + return pos + 1, array + while True: + pos, val = parse_value(src, pos, parse_float) + array.append(val) + pos = skip_comments_and_array_ws(src, pos) + + c = src[pos : pos + 1] + if c == "]": + return pos + 1, array + if c != ",": + raise suffixed_err(src, pos, "Unclosed array") + pos += 1 + + pos = skip_comments_and_array_ws(src, pos) + if src.startswith("]", pos): + return pos + 1, array + + +def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, dict]: + pos += 1 + nested_dict = NestedDict() + flags = Flags() + + pos = skip_chars(src, pos, TOML_WS) + if src.startswith("}", pos): + return pos + 1, nested_dict.dict + while True: + pos, key, value = parse_key_value_pair(src, pos, parse_float) + key_parent, key_stem = key[:-1], key[-1] + if flags.is_(key, Flags.FROZEN): + raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}") + try: + nest = nested_dict.get_or_create_nest(key_parent, access_lists=False) + except KeyError: + raise suffixed_err(src, pos, "Can not overwrite a value") from None + if key_stem in nest: + raise suffixed_err(src, pos, f"Duplicate inline table key {key_stem!r}") + nest[key_stem] = value + pos = skip_chars(src, pos, TOML_WS) + c = src[pos : pos + 1] + if c == "}": + return pos + 1, nested_dict.dict + if c != ",": + raise suffixed_err(src, pos, "Unclosed inline table") + if isinstance(value, (dict, list)): + flags.set(key, Flags.FROZEN, recursive=True) + pos += 1 + pos = skip_chars(src, pos, TOML_WS) + + +def parse_basic_str_escape( # noqa: C901 + src: str, pos: Pos, *, multiline: bool = False +) -> Tuple[Pos, str]: + escape_id = src[pos : pos + 2] + pos += 2 + if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}: + # Skip whitespace until next non-whitespace character or end of + # the doc. Error if non-whitespace is found before newline. + if escape_id != "\\\n": + pos = skip_chars(src, pos, TOML_WS) + try: + char = src[pos] + except IndexError: + return pos, "" + if char != "\n": + raise suffixed_err(src, pos, 'Unescaped "\\" in a string') + pos += 1 + pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE) + return pos, "" + if escape_id == "\\u": + return parse_hex_char(src, pos, 4) + if escape_id == "\\U": + return parse_hex_char(src, pos, 8) + try: + return pos, BASIC_STR_ESCAPE_REPLACEMENTS[escape_id] + except KeyError: + if len(escape_id) != 2: + raise suffixed_err(src, pos, "Unterminated string") from None + raise suffixed_err(src, pos, 'Unescaped "\\" in a string') from None + + +def parse_basic_str_escape_multiline(src: str, pos: Pos) -> Tuple[Pos, str]: + return parse_basic_str_escape(src, pos, multiline=True) + + +def parse_hex_char(src: str, pos: Pos, hex_len: int) -> Tuple[Pos, str]: + hex_str = src[pos : pos + hex_len] + if len(hex_str) != hex_len or not HEXDIGIT_CHARS.issuperset(hex_str): + raise suffixed_err(src, pos, "Invalid hex value") + pos += hex_len + hex_int = int(hex_str, 16) + if not is_unicode_scalar_value(hex_int): + raise suffixed_err(src, pos, "Escaped character is not a Unicode scalar value") + return pos, chr(hex_int) + + +def parse_literal_str(src: str, pos: Pos) -> Tuple[Pos, str]: + pos += 1 # Skip starting apostrophe + start_pos = pos + pos = skip_until( + src, pos, "'", error_on=ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True + ) + return pos + 1, src[start_pos:pos] # Skip ending apostrophe + + +def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> Tuple[Pos, str]: + pos += 3 + if src.startswith("\n", pos): + pos += 1 + + if literal: + delim = "'" + end_pos = skip_until( + src, + pos, + "'''", + error_on=ILLEGAL_MULTILINE_LITERAL_STR_CHARS, + error_on_eof=True, + ) + result = src[pos:end_pos] + pos = end_pos + 3 + else: + delim = '"' + pos, result = parse_basic_str(src, pos, multiline=True) + + # Add at maximum two extra apostrophes/quotes if the end sequence + # is 4 or 5 chars long instead of just 3. + if not src.startswith(delim, pos): + return pos, result + pos += 1 + if not src.startswith(delim, pos): + return pos, result + delim + pos += 1 + return pos, result + (delim * 2) + + +def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> Tuple[Pos, str]: + if multiline: + error_on = ILLEGAL_MULTILINE_BASIC_STR_CHARS + parse_escapes = parse_basic_str_escape_multiline + else: + error_on = ILLEGAL_BASIC_STR_CHARS + parse_escapes = parse_basic_str_escape + result = "" + start_pos = pos + while True: + try: + char = src[pos] + except IndexError: + raise suffixed_err(src, pos, "Unterminated string") from None + if char == '"': + if not multiline: + return pos + 1, result + src[start_pos:pos] + if src.startswith('"""', pos): + return pos + 3, result + src[start_pos:pos] + pos += 1 + continue + if char == "\\": + result += src[start_pos:pos] + pos, parsed_escape = parse_escapes(src, pos) + result += parsed_escape + start_pos = pos + continue + if char in error_on: + raise suffixed_err(src, pos, f"Illegal character {char!r}") + pos += 1 + + +def parse_value( # noqa: C901 + src: str, pos: Pos, parse_float: ParseFloat +) -> Tuple[Pos, Any]: + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + + # Basic strings + if char == '"': + if src.startswith('"""', pos): + return parse_multiline_str(src, pos, literal=False) + return parse_one_line_basic_str(src, pos) + + # Literal strings + if char == "'": + if src.startswith("'''", pos): + return parse_multiline_str(src, pos, literal=True) + return parse_literal_str(src, pos) + + # Booleans + if char == "t": + if src.startswith("true", pos): + return pos + 4, True + if char == "f": + if src.startswith("false", pos): + return pos + 5, False + + # Dates and times + datetime_match = RE_DATETIME.match(src, pos) + if datetime_match: + try: + datetime_obj = match_to_datetime(datetime_match) + except ValueError as e: + raise suffixed_err(src, pos, "Invalid date or datetime") from e + return datetime_match.end(), datetime_obj + localtime_match = RE_LOCALTIME.match(src, pos) + if localtime_match: + return localtime_match.end(), match_to_localtime(localtime_match) + + # Integers and "normal" floats. + # The regex will greedily match any type starting with a decimal + # char, so needs to be located after handling of dates and times. + number_match = RE_NUMBER.match(src, pos) + if number_match: + return number_match.end(), match_to_number(number_match, parse_float) + + # Arrays + if char == "[": + return parse_array(src, pos, parse_float) + + # Inline tables + if char == "{": + return parse_inline_table(src, pos, parse_float) + + # Special floats + first_three = src[pos : pos + 3] + if first_three in {"inf", "nan"}: + return pos + 3, parse_float(first_three) + first_four = src[pos : pos + 4] + if first_four in {"-inf", "+inf", "-nan", "+nan"}: + return pos + 4, parse_float(first_four) + + raise suffixed_err(src, pos, "Invalid value") + + +def suffixed_err(src: str, pos: Pos, msg: str) -> TOMLDecodeError: + """Return a `TOMLDecodeError` where error message is suffixed with + coordinates in source.""" + + def coord_repr(src: str, pos: Pos) -> str: + if pos >= len(src): + return "end of document" + line = src.count("\n", 0, pos) + 1 + if line == 1: + column = pos + 1 + else: + column = pos - src.rindex("\n", 0, pos) + return f"line {line}, column {column}" + + return TOMLDecodeError(f"{msg} (at {coord_repr(src, pos)})") + + +def is_unicode_scalar_value(codepoint: int) -> bool: + return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111) diff --git a/flit_core/flit_core/vendor/tomli/_re.py b/flit_core/flit_core/vendor/tomli/_re.py new file mode 100644 index 00000000..45e17e2c --- /dev/null +++ b/flit_core/flit_core/vendor/tomli/_re.py @@ -0,0 +1,101 @@ +from datetime import date, datetime, time, timedelta, timezone, tzinfo +from functools import lru_cache +import re +from typing import Any, Optional, Union + +from ._types import ParseFloat + +# E.g. +# - 00:32:00.999999 +# - 00:32:00 +_TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?" + +RE_NUMBER = re.compile( + r""" +0 +(?: + x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex + | + b[01](?:_?[01])* # bin + | + o[0-7](?:_?[0-7])* # oct +) +| +[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part +(?P + (?:\.[0-9](?:_?[0-9])*)? # optional fractional part + (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part +) +""", + flags=re.VERBOSE, +) +RE_LOCALTIME = re.compile(_TIME_RE_STR) +RE_DATETIME = re.compile( + fr""" +([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27 +(?: + [Tt ] + {_TIME_RE_STR} + (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset +)? +""", + flags=re.VERBOSE, +) + + +def match_to_datetime(match: "re.Match") -> Union[datetime, date]: + """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`. + + Raises ValueError if the match does not correspond to a valid date + or datetime. + """ + ( + year_str, + month_str, + day_str, + hour_str, + minute_str, + sec_str, + micros_str, + zulu_time, + offset_sign_str, + offset_hour_str, + offset_minute_str, + ) = match.groups() + year, month, day = int(year_str), int(month_str), int(day_str) + if hour_str is None: + return date(year, month, day) + hour, minute, sec = int(hour_str), int(minute_str), int(sec_str) + micros = int(micros_str.ljust(6, "0")) if micros_str else 0 + if offset_sign_str: + tz: Optional[tzinfo] = cached_tz( + offset_hour_str, offset_minute_str, offset_sign_str + ) + elif zulu_time: + tz = timezone.utc + else: # local date-time + tz = None + return datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz) + + +@lru_cache(maxsize=None) +def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone: + sign = 1 if sign_str == "+" else -1 + return timezone( + timedelta( + hours=sign * int(hour_str), + minutes=sign * int(minute_str), + ) + ) + + +def match_to_localtime(match: "re.Match") -> time: + hour_str, minute_str, sec_str, micros_str = match.groups() + micros = int(micros_str.ljust(6, "0")) if micros_str else 0 + return time(int(hour_str), int(minute_str), int(sec_str), micros) + + +def match_to_number(match: "re.Match", parse_float: "ParseFloat") -> Any: + if match.group("floatpart"): + return parse_float(match.group()) + return int(match.group(), 0) diff --git a/flit_core/flit_core/vendor/tomli/_types.py b/flit_core/flit_core/vendor/tomli/_types.py new file mode 100644 index 00000000..e37cc808 --- /dev/null +++ b/flit_core/flit_core/vendor/tomli/_types.py @@ -0,0 +1,6 @@ +from typing import Any, Callable, Tuple + +# Type annotations +ParseFloat = Callable[[str], Any] +Key = Tuple[str, ...] +Pos = int diff --git a/flit_core/flit_core/vendor/tomli/py.typed b/flit_core/flit_core/vendor/tomli/py.typed new file mode 100644 index 00000000..7632ecf7 --- /dev/null +++ b/flit_core/flit_core/vendor/tomli/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 From 6e43d520f4a63390c60854ad26340f960eca0c04 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 23 Dec 2021 10:50:20 +0000 Subject: [PATCH 07/14] Use bundled copy of tomli in flit_core --- flit_core/flit_core/build_thyself.py | 4 +--- flit_core/flit_core/config.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/flit_core/flit_core/build_thyself.py b/flit_core/flit_core/build_thyself.py index 2688e842..f0e8f6fe 100644 --- a/flit_core/flit_core/build_thyself.py +++ b/flit_core/flit_core/build_thyself.py @@ -25,9 +25,7 @@ "summary": ( "Distribution-building parts of Flit. " "See flit package for more information" ), - "requires_dist": [ - "tomli", - ], + "requires_dist": [], 'requires_python': '>=3.6', 'classifiers': [ "License :: OSI Approved :: BSD License", diff --git a/flit_core/flit_core/config.py b/flit_core/flit_core/config.py index b89d7ff6..b0b6ddbd 100644 --- a/flit_core/flit_core/config.py +++ b/flit_core/flit_core/config.py @@ -5,9 +5,9 @@ import os import os.path as osp from pathlib import Path -import tomli import re +from .vendor import tomli from .versionno import normalise_version log = logging.getLogger(__name__) From 55053217f09e052703c49b8812e7fa1a1a8359f4 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 23 Dec 2021 11:06:41 +0000 Subject: [PATCH 08/14] Document bundling --- doc/bootstrap.rst | 8 +++----- flit_core/flit_core/vendor/README | 13 +++++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 flit_core/flit_core/vendor/README diff --git a/doc/bootstrap.rst b/doc/bootstrap.rst index 6f1e3695..0fce7250 100644 --- a/doc/bootstrap.rst +++ b/doc/bootstrap.rst @@ -25,11 +25,9 @@ building other packages. (You could also just copy ``flit_core`` from the source directory, but without the ``.dist-info`` folder, tools like pip won't know that it's installed.) -Note that although ``flit_core`` has no *build* dependencies, it has one runtime -dependency, `Tomli `_. Tomli is itself packaged -with Flit, so after building ``flit_core``, you will need to use that to build -Tomli, arranging for ``tomli`` to be importable directly from the source location -(e.g. using the ``PYTHONPATH`` environment variable). +As of version 3.6, flit_core bundles the ``tomli`` TOML parser, to avoid a +dependency cycle. If you need to unbundle it, you will need to special-case +installing flit_core and/or tomli to get around that cycle. I recommend that you get the `build `_ and `installer `_ packages (and their diff --git a/flit_core/flit_core/vendor/README b/flit_core/flit_core/vendor/README new file mode 100644 index 00000000..32e1b009 --- /dev/null +++ b/flit_core/flit_core/vendor/README @@ -0,0 +1,13 @@ +flit_core bundles the 'tomli' TOML parser, to avoid a bootstrapping problem. +tomli is packaged using Flit, so there would be a dependency cycle when building +from source. Vendoring a copy of tomli avoids this. The code in tomli is under +the MIT license, and the LICENSE file is in the .dist-info folder. + +If you want to unbundle tomli and rely on it as a separate package, you can +replace the package with Python code doing 'from tomli import *'. You will +probably need to work around the dependency cycle between flit_core and tomli. + +Bundling a TOML parser should be a special case - I don't plan on bundling +anything else in flit_core (or depending on any other packages). +I hope that a TOML parser will be added to the Python standard library, and then +this bundled parser will go away. From 0ae76b209158df9e1a4a986d35776772e62495cc Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 23 Dec 2021 11:17:01 +0000 Subject: [PATCH 09/14] Special build_thyself API no longer needed - flit_core can build itself from TOML file --- bootstrap_dev.py | 12 +-- doc/bootstrap.rst | 4 +- flit_core/build_dists.py | 6 +- flit_core/flit_core/build_thyself.py | 94 ------------------- .../flit_core/tests/test_build_thyself.py | 13 +-- flit_core/pyproject.toml | 20 +++- tox.ini | 2 +- 7 files changed, 32 insertions(+), 119 deletions(-) delete mode 100644 flit_core/flit_core/build_thyself.py diff --git a/bootstrap_dev.py b/bootstrap_dev.py index 4f6df3da..e7cf3e14 100644 --- a/bootstrap_dev.py +++ b/bootstrap_dev.py @@ -14,8 +14,6 @@ os.chdir(str(my_dir)) sys.path.insert(0, 'flit_core') -from flit_core import build_thyself -from flit_core.config import LoadedConfig from flit.install import Installer ap = argparse.ArgumentParser() @@ -24,20 +22,14 @@ logging.basicConfig(level=logging.INFO) -# Construct config for flit_core -core_config = LoadedConfig() -core_config.module = 'flit_core' -core_config.metadata = build_thyself.metadata_dict -core_config.reqs_by_extra['.none'] = build_thyself.metadata.requires_dist - install_kwargs = {'symlink': True} if os.name == 'nt': # Use .pth files instead of symlinking on Windows install_kwargs = {'symlink': False, 'pth': True} # Install flit_core -Installer( - my_dir / 'flit_core', core_config, user=args.user, **install_kwargs +Installer.from_ini_path( + my_dir / 'flit_core' / 'pyproject.toml', user=args.user, **install_kwargs ).install() print("Linked flit_core into site-packages.") diff --git a/doc/bootstrap.rst b/doc/bootstrap.rst index 0fce7250..02da8945 100644 --- a/doc/bootstrap.rst +++ b/doc/bootstrap.rst @@ -15,8 +15,8 @@ The key piece is ``flit_core``. This is a package which can build itself using nothing except Python and the standard library. From an unpacked source archive, you can run ``python build_dists.py``, of which the crucial part is:: - from flit_core import build_thyself - whl_fname = build_thyself.build_wheel('dist/') + from flit_core import buildapi + whl_fname = buildapi.build_wheel('dist/') print(os.path.join('dist', whl_fname)) This produces a ``.whl`` wheel file, which you can unzip into your diff --git a/flit_core/build_dists.py b/flit_core/build_dists.py index 07fe1c91..efbce596 100644 --- a/flit_core/build_dists.py +++ b/flit_core/build_dists.py @@ -4,14 +4,14 @@ """ import os -from flit_core import build_thyself +from flit_core import buildapi os.chdir(os.path.dirname(os.path.abspath(__file__))) print("Building sdist") -sdist_fname = build_thyself.build_sdist('dist/') +sdist_fname = buildapi.build_sdist('dist/') print(os.path.join('dist', sdist_fname)) print("\nBuilding wheel") -whl_fname = build_thyself.build_wheel('dist/') +whl_fname = buildapi.build_wheel('dist/') print(os.path.join('dist', whl_fname)) diff --git a/flit_core/flit_core/build_thyself.py b/flit_core/flit_core/build_thyself.py deleted file mode 100644 index f0e8f6fe..00000000 --- a/flit_core/flit_core/build_thyself.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Bootstrapping backend - -This is *only* meant to build flit_core itself. -Building any other packages occurs through flit_core.buildapi -""" - -import io -import os -import os.path as osp -from pathlib import Path -import tempfile - -from .common import Metadata, Module, dist_info_name -from .wheel import WheelBuilder, _write_wheel_file -from .sdist import SdistBuilder - -from . import __version__ - -metadata_dict = { - "name": "flit_core", - "version": __version__, - "author": "Thomas Kluyver & contributors", - "author_email": "thomas@kluyver.me.uk", - "home_page": "https://github.com/pypa/flit", - "summary": ( - "Distribution-building parts of Flit. " "See flit package for more information" - ), - "requires_dist": [], - 'requires_python': '>=3.6', - 'classifiers': [ - "License :: OSI Approved :: BSD License", - "Topic :: Software Development :: Libraries :: Python Modules", - ] -} -metadata = Metadata(metadata_dict) - -def get_requires_for_build_wheel(config_settings=None): - """Returns a list of requirements for building, as strings""" - return [] - -def get_requires_for_build_sdist(config_settings=None): - """Returns a list of requirements for building, as strings""" - return [] - -def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): - """Creates {metadata_directory}/foo-1.2.dist-info""" - dist_info = osp.join(metadata_directory, - dist_info_name(metadata.name, metadata.version)) - os.mkdir(dist_info) - - with open(osp.join(dist_info, 'WHEEL'), 'w') as f: - _write_wheel_file(f, supports_py2=metadata.supports_py2) - - with open(osp.join(dist_info, 'METADATA'), 'w') as f: - metadata.write_metadata_file(f) - - return osp.basename(dist_info) - -def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): - """Builds a wheel, places it in wheel_directory""" - cwd = Path.cwd() - module = Module('flit_core', cwd) - - # We don't know the final filename until metadata is loaded, so write to - # a temporary_file, and rename it afterwards. - (fd, temp_path) = tempfile.mkstemp(suffix='.whl', dir=str(wheel_directory)) - try: - with io.open(fd, 'w+b') as fp: - wb = WheelBuilder( - cwd, module, metadata, entrypoints={}, target_fp=fp - ) - wb.build() - - wheel_path = osp.join(wheel_directory, wb.wheel_filename) - os.replace(temp_path, wheel_path) - except: - os.unlink(temp_path) - raise - - return wb.wheel_filename - -def build_sdist(sdist_directory, config_settings=None): - """Builds an sdist, places it in sdist_directory""" - cwd = Path.cwd() - module = Module('flit_core', cwd) - reqs_by_extra = {'.none': metadata.requires} - - sb = SdistBuilder( - module, metadata, cwd, reqs_by_extra, entrypoints={}, - extra_files=['pyproject.toml', 'build_dists.py'] - ) - path = sb.build(Path(sdist_directory)) - return path.name - diff --git a/flit_core/flit_core/tests/test_build_thyself.py b/flit_core/flit_core/tests/test_build_thyself.py index 10daabc0..ad15819d 100644 --- a/flit_core/flit_core/tests/test_build_thyself.py +++ b/flit_core/flit_core/tests/test_build_thyself.py @@ -1,3 +1,4 @@ +"""Tests of flit_core building itself""" import os import os.path as osp import pytest @@ -5,11 +6,11 @@ from testpath import assert_isdir, assert_isfile import zipfile -from flit_core import build_thyself +from flit_core import buildapi @pytest.fixture() def cwd_project(): - proj_dir = osp.dirname(osp.dirname(osp.abspath(build_thyself.__file__))) + proj_dir = osp.dirname(osp.dirname(osp.abspath(buildapi.__file__))) if not osp.isfile(osp.join(proj_dir, 'pyproject.toml')): pytest.skip("need flit_core source directory") @@ -21,9 +22,9 @@ def cwd_project(): os.chdir(old_cwd) -def test_prepare_metadata(tmp_path): +def test_prepare_metadata(tmp_path, cwd_project): tmp_path = str(tmp_path) - dist_info = build_thyself.prepare_metadata_for_build_wheel(tmp_path) + dist_info = buildapi.prepare_metadata_for_build_wheel(tmp_path) assert dist_info.endswith('.dist-info') assert dist_info.startswith('flit_core') @@ -36,7 +37,7 @@ def test_prepare_metadata(tmp_path): def test_wheel(tmp_path, cwd_project): tmp_path = str(tmp_path) - filename = build_thyself.build_wheel(tmp_path) + filename = buildapi.build_wheel(tmp_path) assert filename.endswith('.whl') assert filename.startswith('flit_core') @@ -47,7 +48,7 @@ def test_wheel(tmp_path, cwd_project): def test_sdist(tmp_path, cwd_project): tmp_path = str(tmp_path) - filename = build_thyself.build_sdist(tmp_path) + filename = buildapi.build_sdist(tmp_path) assert filename.endswith('.tar.gz') assert filename.startswith('flit_core') diff --git a/flit_core/pyproject.toml b/flit_core/pyproject.toml index a1ca5a37..ca3f46b2 100644 --- a/flit_core/pyproject.toml +++ b/flit_core/pyproject.toml @@ -1,7 +1,21 @@ [build-system] requires = [] -build-backend = "flit_core.build_thyself" +build-backend = "flit_core.buildapi" backend-path = ["."] -# Runtime dependencies & other metadata are listed in flit_core/build_thyself.py -# https://github.com/pypa/flit/issues/482#issuecomment-987769343 +[project] +name="flit_core" +authors=[ + {name = "Thomas Kluyver & contributors", email = "thomas@kluyver.me.uk"}, +] +description = "Distribution-building parts of Flit. See flit package for more information" +dependencies = [] +requires-python = '>=3.6' +classifiers = [ + "License :: OSI Approved :: BSD License", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dynamic = ["version"] + +[project.urls] +Source = "https://github.com/pypa/flit" diff --git a/tox.ini b/tox.ini index c900763a..71bc8867 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,6 @@ install_command = true {packages} whitelist_externals = true changedir = flit_core commands = - python -c "from flit_core.build_thyself import build_wheel;\ + python -c "from flit_core.buildapi import build_wheel;\ from tempfile import mkdtemp;\ build_wheel(mkdtemp())" From 2636b951c4535bdda7f61389a52961e4ac3e29db Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 23 Dec 2021 11:43:44 +0000 Subject: [PATCH 10/14] Exclude vendored tomli from coverage measurement --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 321e5ca0..f151be6a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,3 @@ [run] omit = */tests/* + flit_core/flit_core/vendor From 06b69154b83e4aceabe46a8f48fe139aacb6a214 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 23 Dec 2021 11:48:51 +0000 Subject: [PATCH 11/14] Try again to ignore vendored tomli in coverage --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index f151be6a..a24883e8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,3 @@ [run] omit = */tests/* - flit_core/flit_core/vendor + */flit_core/vendor/* From bb7430ab1387803c90e5a714c6a822e5fba133af Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 24 Dec 2021 11:05:20 +0000 Subject: [PATCH 12/14] Remove messages about setup.py default changing (#493) --- flit/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/flit/__init__.py b/flit/__init__.py index 4700a6ed..3d059b71 100644 --- a/flit/__init__.py +++ b/flit/__init__.py @@ -168,13 +168,6 @@ def main(argv=None): def gen_setup_py(): if not (args.setup_py or args.no_setup_py): - log.info("Not generating setup.py in sdist (default changed)") - log.info( - "Recent versions of pip no longer need this generated file" - ) - log.info( - "Use --[no-]setup-py to suppress this message or add setup.py" - ) return False return args.setup_py From 3d33eb21824831d5fb15d035be29a388f9275db8 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 27 Dec 2021 13:38:21 +0000 Subject: [PATCH 13/14] Add release notes for version 3.6 --- doc/history.rst | 9 +++++++++ pyproject.toml | 1 + 2 files changed, 10 insertions(+) diff --git a/doc/history.rst b/doc/history.rst index e3aa6e44..e3e6a126 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -1,6 +1,15 @@ Release history =============== +Version 3.6 +----------- + +- ``flit_core`` now bundles the `tomli `_ TOML + parser library (version 1.2.3) to avoid a circular dependency between + ``flit_core`` and ``tomli`` (:ghpull:`492`). This means ``flit_core`` now has + no dependencies except Python itself, both at build time and at runtime, + simplifying :doc:`bootstrapping `. + Version 3.5.1 ------------- diff --git a/pyproject.toml b/pyproject.toml index fe6d3432..fb30594d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ doc = [ [project.urls] Documentation = "https://flit.readthedocs.io/en/latest/" Source = "https://github.com/pypa/flit" +Changelog = "https://flit.readthedocs.io/en/latest/history.html" [project.scripts] flit = "flit:main" From 68ed8526b4293d4c8b9bc9bb41af07599a009db4 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 27 Dec 2021 13:38:33 +0000 Subject: [PATCH 14/14] =?UTF-8?q?Bump=20version:=203.5.1=20=E2=86=92=203.6?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- doc/conf.py | 2 +- flit/__init__.py | 2 +- flit_core/flit_core/__init__.py | 2 +- pyproject.toml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e0448d9b..f8280ed8 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.5.1 +current_version = 3.6.0 commit = True tag = False diff --git a/doc/conf.py b/doc/conf.py index 161841d5..2837ce50 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -57,7 +57,7 @@ # built documents. # # The short X.Y version. -version = '3.5.1' +version = '3.6.0' # The full version, including alpha/beta/rc tags. release = version #+ '.1' diff --git a/flit/__init__.py b/flit/__init__.py index 3d059b71..8af9b34b 100644 --- a/flit/__init__.py +++ b/flit/__init__.py @@ -12,7 +12,7 @@ from .config import ConfigError from .log import enable_colourful_output -__version__ = '3.5.1' +__version__ = '3.6.0' log = logging.getLogger(__name__) diff --git a/flit_core/flit_core/__init__.py b/flit_core/flit_core/__init__.py index 0d464a19..0314577d 100644 --- a/flit_core/flit_core/__init__.py +++ b/flit_core/flit_core/__init__.py @@ -4,4 +4,4 @@ All the convenient development features live in the main 'flit' package. """ -__version__ = '3.5.1' +__version__ = '3.6.0' diff --git a/pyproject.toml b/pyproject.toml index fb30594d..64d14150 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["flit_core >=3.5.1,<4"] +requires = ["flit_core >=3.6.0,<4"] build-backend = "flit_core.buildapi" [project] @@ -8,7 +8,7 @@ authors = [ {name = "Thomas Kluyver", email = "thomas@kluyver.me.uk"}, ] dependencies = [ - "flit_core >=3.5.1", + "flit_core >=3.6.0", "requests", "docutils", "tomli",