Compare commits
50
Commits
2025.8.202
...
bug_fixing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfe15dd3db | ||
|
|
3b233389c8 | ||
|
|
f60c4563e4 | ||
|
|
3ad3847045 | ||
|
|
41f331d4c4 | ||
|
|
91ce762570 | ||
|
|
d60d3ec942 | ||
|
|
e52722e353 | ||
|
|
a8bf55882e | ||
|
|
377bddf579 | ||
|
|
cdc1cb4805 | ||
|
|
34430097d8 | ||
|
|
dcf447ed4f | ||
|
|
783a9aa7d7 | ||
|
|
63638d6b3b | ||
|
|
af8cd58fda | ||
|
|
df0f3d7a61 | ||
|
|
72906a1930 | ||
|
|
068cf2d1c3 | ||
|
|
ad18a3dcf3 | ||
|
|
bd4b17825c | ||
|
|
5d6182e91f | ||
|
|
63e9c429bd | ||
|
|
7fdafe484e | ||
|
|
5cba6763f1 | ||
|
|
e20bb85ac6 | ||
|
|
98a0b2197d | ||
|
|
480978d6bb | ||
|
|
04798328bd | ||
|
|
ae835089b6 | ||
|
|
0ed1e887fc | ||
|
|
12b7b62aa5 | ||
|
|
7263b6f530 | ||
|
|
c0635e48ea | ||
|
|
8fb98d2f40 | ||
|
|
f24b280edf | ||
|
|
b61cff3b3b | ||
|
|
feba88212f | ||
|
|
b141818251 | ||
|
|
72f894b05d | ||
|
|
0a26cb5c0f | ||
|
|
b3cac5c292 | ||
|
|
ef558c3aa3 | ||
|
|
9d5ef23162 | ||
|
|
c25bf79427 | ||
|
|
a90e15514b | ||
|
|
2c2579319d | ||
|
|
b38fab1428 | ||
|
|
bc745622b9 | ||
|
|
0bb4ef731e |
@@ -5,8 +5,11 @@ jobs:
|
||||
build_and_publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
- name: Checkout branch from release
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.target_commitish }}
|
||||
fetch-depth: 0
|
||||
- name: GitHub Tag Name example
|
||||
run: |
|
||||
echo "Tag name from GITHUB_REF_NAME: $GITHUB_REF_NAME"
|
||||
@@ -32,8 +35,25 @@ jobs:
|
||||
git config user.name "${{ vars.USERNAME_GIT }}"
|
||||
git config user.email "${{ vars.EMAIL_GIT }}"
|
||||
git add package.json
|
||||
git commit -m "Update version to ${{ github.ref_name }}"
|
||||
git push origin HEAD:main
|
||||
git commit -m "Update version to ${{ github.ref_name }}" || echo "No changes to commit."
|
||||
|
||||
# Get current branch; if detached, resolve to a remote branch that contains this commit
|
||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [[ "$BRANCH" == "HEAD" || -z "$BRANCH" ]]; then
|
||||
# If target_commitish is a branch, use it; if it’s a SHA, find a branch that contains it
|
||||
CANDIDATE='${{ github.event.release.target_commitish }}'
|
||||
if [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
BRANCH="$(git branch -r --contains "$GITHUB_SHA" | sed -n 's|.*origin/||p' | head -n1)"
|
||||
else
|
||||
BRANCH="$CANDIDATE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$BRANCH" ]]; then
|
||||
echo "Could not determine branch for push."; git branch -a; exit 1
|
||||
fi
|
||||
|
||||
git push origin HEAD:refs/heads/$BRANCH
|
||||
- name: Publish to Visual Studio Marketplace (Pre-Release)
|
||||
if: ${{ github.event.release.prerelease }}
|
||||
run: vsce publish --pre-release
|
||||
|
||||
+2
-1
@@ -8,4 +8,5 @@ __pycache__
|
||||
*.pyc
|
||||
.nox
|
||||
*.g4
|
||||
.antlr
|
||||
.antlr
|
||||
.claude
|
||||
+9
-1
@@ -12,4 +12,12 @@
|
||||
|
||||
## [0.2.0]
|
||||
|
||||
- Add DEF File Support
|
||||
- Add DEF File Support
|
||||
|
||||
## [2026.6.100]
|
||||
|
||||
- Fix several bugs
|
||||
|
||||
## [2026.6.200]
|
||||
|
||||
- Fix foramtting bug
|
||||
|
||||
@@ -1,21 +1,661 @@
|
||||
MIT License
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (c) 2025 Christoph
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
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:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server 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.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
nx-post-support
|
||||
Copyright (C) 2025 Christoph
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# NX Postprocessor Support
|
||||
|
||||
A comprehensive VS Code extension providing language support for NX CAD postprocessor development, including CDL (Cutter Definition Language), TCL, and DEF files.
|
||||
A comprehensive VS Code extension providing language support for NX CAM postprocessor development, including CDL, TCL, and DEF files.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -12,15 +12,16 @@ A comprehensive VS Code extension providing language support for NX CAD postproc
|
||||
|
||||
## Supported File Types
|
||||
|
||||
- `.cdl` - NX Cutter Definition Language files
|
||||
- `.cdl` - NX User Defined Event File
|
||||
- `.tcl` - NX TCL postprocessor files
|
||||
- `.def` - NX Definition files
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install from the VS Code Marketplace
|
||||
2. Open any `.cdl`, `.tcl`, or `.def` file
|
||||
3. The extension will automatically activate and provide language support
|
||||
2. Install Python 3.8 or higher
|
||||
3. Open any `.cdl`, `.tcl`, or `.def` file
|
||||
4. The extension will automatically activate and provide language support
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -46,4 +47,4 @@ This extension is actively maintained. For issues or feature requests, please vi
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
AGPL-3.0 License - see LICENSE file for details.
|
||||
|
||||
@@ -271,3 +271,180 @@ export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode
|
||||
|
||||
return collectSymbols(rootScope)
|
||||
}
|
||||
|
||||
export function cdlDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
|
||||
const lines = document.getText().split("\n")
|
||||
|
||||
interface Scope {
|
||||
symbol: vscode.DocumentSymbol
|
||||
startLine: number
|
||||
}
|
||||
|
||||
const root = new vscode.DocumentSymbol(
|
||||
"root",
|
||||
"",
|
||||
vscode.SymbolKind.Namespace,
|
||||
new vscode.Range(0, 0, lines.length, 0),
|
||||
new vscode.Range(0, 0, 0, 0)
|
||||
)
|
||||
root.children = []
|
||||
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
|
||||
|
||||
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const trimmed = line.trim()
|
||||
|
||||
const createSymbol = () => {
|
||||
if (!pending) {
|
||||
return
|
||||
}
|
||||
const startRange = new vscode.Range(
|
||||
pending.startLine,
|
||||
0,
|
||||
pending.startLine,
|
||||
lines[pending.startLine].length
|
||||
)
|
||||
const symbol = new vscode.DocumentSymbol(
|
||||
pending.name,
|
||||
"",
|
||||
pending.kind,
|
||||
startRange,
|
||||
startRange
|
||||
)
|
||||
symbol.children = []
|
||||
stack[stack.length - 1].symbol.children?.push(symbol)
|
||||
stack.push({ symbol, startLine: pending.startLine })
|
||||
pending = null
|
||||
}
|
||||
|
||||
if (pending && trimmed.includes("{")) {
|
||||
createSymbol()
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("}")) {
|
||||
const scope = stack.pop()
|
||||
if (scope) {
|
||||
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
let match
|
||||
if ((match = /^\s*EVENT\s+(\w+)/.exec(line))) {
|
||||
pending = {
|
||||
name: match[1],
|
||||
kind: vscode.SymbolKind.Event,
|
||||
startLine: i
|
||||
}
|
||||
if (line.includes("{")) {
|
||||
createSymbol()
|
||||
}
|
||||
} else if ((match = /^\s*PARAM\s+(\w+)/.exec(line))) {
|
||||
pending = {
|
||||
name: match[1],
|
||||
kind: vscode.SymbolKind.Field,
|
||||
startLine: i
|
||||
}
|
||||
if (line.includes("{")) {
|
||||
createSymbol()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root.children || []
|
||||
}
|
||||
|
||||
export function defDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
|
||||
const lines = document.getText().split("\n")
|
||||
|
||||
interface Scope {
|
||||
symbol: vscode.DocumentSymbol
|
||||
startLine: number
|
||||
}
|
||||
|
||||
const root = new vscode.DocumentSymbol(
|
||||
"root",
|
||||
"",
|
||||
vscode.SymbolKind.Namespace,
|
||||
new vscode.Range(0, 0, lines.length, 0),
|
||||
new vscode.Range(0, 0, 0, 0)
|
||||
)
|
||||
root.children = []
|
||||
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
|
||||
|
||||
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const trimmed = line.trim()
|
||||
|
||||
const createSymbol = () => {
|
||||
if (!pending) {
|
||||
return
|
||||
}
|
||||
const startRange = new vscode.Range(
|
||||
pending.startLine,
|
||||
0,
|
||||
pending.startLine,
|
||||
lines[pending.startLine].length
|
||||
)
|
||||
const symbol = new vscode.DocumentSymbol(
|
||||
pending.name,
|
||||
"",
|
||||
pending.kind,
|
||||
startRange,
|
||||
startRange
|
||||
)
|
||||
symbol.children = []
|
||||
stack[stack.length - 1].symbol.children?.push(symbol)
|
||||
stack.push({ symbol, startLine: pending.startLine })
|
||||
pending = null
|
||||
}
|
||||
|
||||
if (pending && trimmed.includes("{")) {
|
||||
createSymbol()
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("}")) {
|
||||
const scope = stack.pop()
|
||||
if (scope) {
|
||||
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
let match
|
||||
if ((match = /^\s*FORMATTING\b/.exec(line))) {
|
||||
pending = {
|
||||
name: "FORMATTING",
|
||||
kind: vscode.SymbolKind.Module,
|
||||
startLine: i
|
||||
}
|
||||
if (line.includes("{")) {
|
||||
createSymbol()
|
||||
}
|
||||
} else if ((match = /^\s*ADDRESS\s+(\w+)/.exec(line))) {
|
||||
pending = {
|
||||
name: match[1],
|
||||
kind: vscode.SymbolKind.Field,
|
||||
startLine: i
|
||||
}
|
||||
if (line.includes("{")) {
|
||||
createSymbol()
|
||||
}
|
||||
} else if ((match = /^\s*BLOCK_TEMPLATE\s+(\w+)/.exec(line))) {
|
||||
pending = {
|
||||
name: match[1],
|
||||
kind: vscode.SymbolKind.Function,
|
||||
startLine: i
|
||||
}
|
||||
if (line.includes("{")) {
|
||||
createSymbol()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root.children || []
|
||||
}
|
||||
|
||||
+56
-25
@@ -12,7 +12,8 @@ import {
|
||||
formatDefFile,
|
||||
isFirstLineMachine,
|
||||
diagnosticHandler,
|
||||
tclDocumentSymbolProvider
|
||||
cdlDocumentSymbolProvider,
|
||||
defDocumentSymbolProvider
|
||||
} from "./common/handlers"
|
||||
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
|
||||
import {
|
||||
@@ -28,7 +29,7 @@ import { loadServerDefaults } from "./common/setup"
|
||||
import { getLSClientTraceLevel } from "./common/utilities"
|
||||
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
|
||||
|
||||
let client: LanguageClient
|
||||
let client: LanguageClient | undefined
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
// This is required to get server name and module. This should be
|
||||
@@ -60,7 +61,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
traceLog(`Module: ${serverInfo.module}`)
|
||||
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`)
|
||||
|
||||
const runServer = async () => {
|
||||
const runServerImpl = async () => {
|
||||
const interpreter = getInterpreterFromSetting(serverId)
|
||||
if (interpreter && interpreter.length > 0) {
|
||||
if (checkVersion(await resolveInterpreter(interpreter))) {
|
||||
@@ -89,6 +90,16 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
)
|
||||
}
|
||||
|
||||
// Serialize server (re)starts. Overlapping triggers (interpreter change,
|
||||
// config change, restart command, initial activation) would otherwise each
|
||||
// read the stale module-level `client`, start a new server and leave the
|
||||
// previous one running orphaned -> hints/hover shown multiple times.
|
||||
let runServerQueue: Promise<void> = Promise.resolve()
|
||||
const runServer = () => {
|
||||
runServerQueue = runServerQueue.catch(() => undefined).then(() => runServerImpl())
|
||||
return runServerQueue
|
||||
}
|
||||
|
||||
context.subscriptions.push(
|
||||
onDidChangePythonInterpreter(async () => {
|
||||
await runServer()
|
||||
@@ -114,6 +125,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
})
|
||||
|
||||
// Folding ranges for TCL are provided by the language server
|
||||
// (folding_range_provider in lsp_server.py). No client-side provider here to
|
||||
// avoid duplicate folding regions.
|
||||
|
||||
//
|
||||
const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider(
|
||||
{ scheme: "file", language: "cdl" },
|
||||
@@ -179,11 +194,26 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
context.subscriptions.push(formatDefProvider)
|
||||
|
||||
const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider(
|
||||
{ scheme: "file", language: "tcl" },
|
||||
{ provideDocumentSymbols: tclDocumentSymbolProvider }
|
||||
const cdlSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
|
||||
{ scheme: "file", language: "cdl" },
|
||||
{
|
||||
provideDocumentSymbols(document: vscode.TextDocument) {
|
||||
return cdlDocumentSymbolProvider(document)
|
||||
}
|
||||
}
|
||||
)
|
||||
context.subscriptions.push(tclOutlineProvider)
|
||||
context.subscriptions.push(cdlSymbolProvider)
|
||||
|
||||
// Outline now provided by the language server (Document Symbols). Removing TS provider.
|
||||
const defSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
|
||||
{ scheme: "file", language: "def" },
|
||||
{
|
||||
provideDocumentSymbols(document: vscode.TextDocument) {
|
||||
return defDocumentSymbolProvider(document)
|
||||
}
|
||||
}
|
||||
)
|
||||
context.subscriptions.push(defSymbolProvider)
|
||||
|
||||
// Diagnostics collection
|
||||
const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl")
|
||||
@@ -191,26 +221,27 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef)
|
||||
|
||||
// Check if the first line of the CDL file contains "MACHINE"
|
||||
vscode.workspace.onDidOpenTextDocument((document) => {
|
||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||
if (document.languageId === "cdl") {
|
||||
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
|
||||
} else if (document.languageId === "def") {
|
||||
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidOpenTextDocument((document) => {
|
||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||
if (document.languageId === "cdl") {
|
||||
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
|
||||
} else if (document.languageId === "def") {
|
||||
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument((event) => {
|
||||
const document = event.document
|
||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||
if (document.languageId === "cdl") {
|
||||
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
|
||||
} else if (document.languageId === "def") {
|
||||
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
||||
}),
|
||||
vscode.workspace.onDidChangeTextDocument((event) => {
|
||||
const document = event.document
|
||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||
if (document.languageId === "cdl") {
|
||||
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
|
||||
} else if (document.languageId === "def") {
|
||||
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> | undefined {
|
||||
|
||||
+33
-26
@@ -1,28 +1,35 @@
|
||||
{
|
||||
"comments": {
|
||||
// symbol used for single line comment. Remove this entry if your language does not support line comments
|
||||
"lineComment": "#"
|
||||
},
|
||||
// symbols used as brackets
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
// symbols that are auto closed when typing
|
||||
"autoClosingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"]
|
||||
],
|
||||
// symbols that can be used to surround a selection
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"]
|
||||
]
|
||||
"comments": {
|
||||
// symbol used for single line comment. Remove this entry if your language does not support line comments
|
||||
"lineComment": "#"
|
||||
},
|
||||
"indentationRules": {
|
||||
// Ignore pure comment lines so they don't terminate folds inside Tcl brace blocks.
|
||||
"unIndentedLinePattern": "^\\s*#.*$",
|
||||
// Opening braces on their own line are common in NX Tcl.
|
||||
"increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*)$",
|
||||
"decreaseIndentPattern": "^\\s*[\\}\\]\\)].*$"
|
||||
},
|
||||
// symbols used as brackets
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
// symbols that are auto closed when typing
|
||||
"autoClosingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"]
|
||||
],
|
||||
// symbols that can be used to surround a selection
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"]
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "nx-post-support",
|
||||
"version": "0.3.0",
|
||||
"version": "2025.9.200",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nx-post-support",
|
||||
"version": "0.3.0",
|
||||
"version": "2025.9.200",
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.96.0",
|
||||
"@vscode/vsce": "^3.2.1",
|
||||
|
||||
+10
-4
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"name": "nx-post-support",
|
||||
"displayName": "NX Postprocessor Support",
|
||||
"description": "",
|
||||
"version": "2025.8.201",
|
||||
"description": "VS Code extension for NX CAM postprocessor development with syntax highlighting, formatting, linting, and auto-completion for CDL, TCL, and DEF files",
|
||||
"version": "2025.9.200",
|
||||
"publisher": "Christoph",
|
||||
"icon": "images/nx-1.png",
|
||||
"extensionDependencies": [
|
||||
"ms-python.python"
|
||||
],
|
||||
"serverInfo": {
|
||||
"name": "NX Postprocessor Support",
|
||||
"module": "nx-post-support"
|
||||
@@ -17,7 +20,10 @@
|
||||
"keywords": [
|
||||
"cdl",
|
||||
"NX CDL",
|
||||
"UDE"
|
||||
"UDE",
|
||||
"Postprocessor",
|
||||
"NX CAM",
|
||||
"Siemens NX"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.96.0"
|
||||
@@ -129,4 +135,4 @@
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ def attrib(
|
||||
type: None = ...,
|
||||
converter: None = ...,
|
||||
factory: None = ...,
|
||||
kw_only: bool = ...,
|
||||
kw_only: bool | None = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
@@ -200,7 +200,7 @@ def attrib(
|
||||
| tuple[_ConverterType]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
kw_only: bool | None = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
@@ -223,7 +223,7 @@ def attrib(
|
||||
| tuple[_ConverterType]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
kw_only: bool | None = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
@@ -246,7 +246,7 @@ def attrib(
|
||||
| tuple[_ConverterType]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
kw_only: bool | None = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
@@ -308,7 +308,7 @@ def attrs(
|
||||
match_args: bool = ...,
|
||||
unsafe_hash: bool | None = ...,
|
||||
) -> Callable[[_C], _C]: ...
|
||||
def fields(cls: type[AttrsInstance]) -> Any: ...
|
||||
def fields(cls: type[AttrsInstance] | AttrsInstance) -> Any: ...
|
||||
def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ...
|
||||
def validate(inst: AttrsInstance) -> None: ...
|
||||
def resolve_types(
|
||||
|
||||
@@ -10,7 +10,6 @@ from typing import _GenericAlias
|
||||
|
||||
|
||||
PYPY = platform.python_implementation() == "PyPy"
|
||||
PY_3_9_PLUS = sys.version_info[:2] >= (3, 9)
|
||||
PY_3_10_PLUS = sys.version_info[:2] >= (3, 10)
|
||||
PY_3_11_PLUS = sys.version_info[:2] >= (3, 11)
|
||||
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
|
||||
@@ -18,10 +17,16 @@ PY_3_13_PLUS = sys.version_info[:2] >= (3, 13)
|
||||
PY_3_14_PLUS = sys.version_info[:2] >= (3, 14)
|
||||
|
||||
|
||||
if PY_3_14_PLUS: # pragma: no cover
|
||||
if PY_3_14_PLUS:
|
||||
import annotationlib
|
||||
|
||||
_get_annotations = annotationlib.get_annotations
|
||||
# We request forward-ref annotations to not break in the presence of
|
||||
# forward references.
|
||||
|
||||
def _get_annotations(cls):
|
||||
return annotationlib.get_annotations(
|
||||
cls, format=annotationlib.Format.FORWARDREF
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
|
||||
+45
-16
@@ -3,11 +3,28 @@
|
||||
|
||||
import copy
|
||||
|
||||
from ._compat import PY_3_9_PLUS, get_generic_base
|
||||
from ._compat import get_generic_base
|
||||
from ._make import _OBJ_SETATTR, NOTHING, fields
|
||||
from .exceptions import AttrsAttributeNotFoundError
|
||||
|
||||
|
||||
_ATOMIC_TYPES = frozenset(
|
||||
{
|
||||
type(None),
|
||||
bool,
|
||||
int,
|
||||
float,
|
||||
str,
|
||||
complex,
|
||||
bytes,
|
||||
type(...),
|
||||
type,
|
||||
range,
|
||||
property,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def asdict(
|
||||
inst,
|
||||
recurse=True,
|
||||
@@ -71,7 +88,10 @@ def asdict(
|
||||
v = value_serializer(inst, a, v)
|
||||
|
||||
if recurse is True:
|
||||
if has(v.__class__):
|
||||
value_type = type(v)
|
||||
if value_type in _ATOMIC_TYPES:
|
||||
rv[a.name] = v
|
||||
elif has(value_type):
|
||||
rv[a.name] = asdict(
|
||||
v,
|
||||
recurse=True,
|
||||
@@ -80,8 +100,8 @@ def asdict(
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
)
|
||||
elif isinstance(v, (tuple, list, set, frozenset)):
|
||||
cf = v.__class__ if retain_collection_types is True else list
|
||||
elif issubclass(value_type, (tuple, list, set, frozenset)):
|
||||
cf = value_type if retain_collection_types is True else list
|
||||
items = [
|
||||
_asdict_anything(
|
||||
i,
|
||||
@@ -101,7 +121,7 @@ def asdict(
|
||||
# Workaround for TypeError: cf.__new__() missing 1 required
|
||||
# positional argument (which appears, for a namedturle)
|
||||
rv[a.name] = cf(*items)
|
||||
elif isinstance(v, dict):
|
||||
elif issubclass(value_type, dict):
|
||||
df = dict_factory
|
||||
rv[a.name] = df(
|
||||
(
|
||||
@@ -142,7 +162,12 @@ def _asdict_anything(
|
||||
"""
|
||||
``asdict`` only works on attrs instances, this works on anything.
|
||||
"""
|
||||
if getattr(val.__class__, "__attrs_attrs__", None) is not None:
|
||||
val_type = type(val)
|
||||
if val_type in _ATOMIC_TYPES:
|
||||
rv = val
|
||||
if value_serializer is not None:
|
||||
rv = value_serializer(None, None, rv)
|
||||
elif getattr(val_type, "__attrs_attrs__", None) is not None:
|
||||
# Attrs class.
|
||||
rv = asdict(
|
||||
val,
|
||||
@@ -152,7 +177,7 @@ def _asdict_anything(
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
)
|
||||
elif isinstance(val, (tuple, list, set, frozenset)):
|
||||
elif issubclass(val_type, (tuple, list, set, frozenset)):
|
||||
if retain_collection_types is True:
|
||||
cf = val.__class__
|
||||
elif is_key:
|
||||
@@ -173,7 +198,7 @@ def _asdict_anything(
|
||||
for i in val
|
||||
]
|
||||
)
|
||||
elif isinstance(val, dict):
|
||||
elif issubclass(val_type, dict):
|
||||
df = dict_factory
|
||||
rv = df(
|
||||
(
|
||||
@@ -253,8 +278,11 @@ def astuple(
|
||||
v = getattr(inst, a.name)
|
||||
if filter is not None and not filter(a, v):
|
||||
continue
|
||||
value_type = type(v)
|
||||
if recurse is True:
|
||||
if has(v.__class__):
|
||||
if value_type in _ATOMIC_TYPES:
|
||||
rv.append(v)
|
||||
elif has(value_type):
|
||||
rv.append(
|
||||
astuple(
|
||||
v,
|
||||
@@ -264,7 +292,7 @@ def astuple(
|
||||
retain_collection_types=retain,
|
||||
)
|
||||
)
|
||||
elif isinstance(v, (tuple, list, set, frozenset)):
|
||||
elif issubclass(value_type, (tuple, list, set, frozenset)):
|
||||
cf = v.__class__ if retain is True else list
|
||||
items = [
|
||||
(
|
||||
@@ -288,8 +316,8 @@ def astuple(
|
||||
# Workaround for TypeError: cf.__new__() missing 1 required
|
||||
# positional argument (which appears, for a namedturle)
|
||||
rv.append(cf(*items))
|
||||
elif isinstance(v, dict):
|
||||
df = v.__class__ if retain is True else dict
|
||||
elif issubclass(value_type, dict):
|
||||
df = value_type if retain is True else dict
|
||||
rv.append(
|
||||
df(
|
||||
(
|
||||
@@ -450,10 +478,11 @@ def resolve_types(
|
||||
if getattr(cls, "__attrs_types_resolved__", None) != cls:
|
||||
import typing
|
||||
|
||||
kwargs = {"globalns": globalns, "localns": localns}
|
||||
|
||||
if PY_3_9_PLUS:
|
||||
kwargs["include_extras"] = include_extras
|
||||
kwargs = {
|
||||
"globalns": globalns,
|
||||
"localns": localns,
|
||||
"include_extras": include_extras,
|
||||
}
|
||||
|
||||
hints = typing.get_type_hints(cls, **kwargs)
|
||||
for field in fields(cls) if attribs is None else attribs:
|
||||
|
||||
+396
-113
@@ -12,6 +12,7 @@ import linecache
|
||||
import sys
|
||||
import types
|
||||
import unicodedata
|
||||
import weakref
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from functools import cached_property
|
||||
@@ -113,7 +114,7 @@ def attrib(
|
||||
type=None,
|
||||
converter=None,
|
||||
factory=None,
|
||||
kw_only=False,
|
||||
kw_only=None,
|
||||
eq=None,
|
||||
order=None,
|
||||
on_setattr=None,
|
||||
@@ -156,6 +157,9 @@ def attrib(
|
||||
*eq*, *order*, and *cmp* also accept a custom callable
|
||||
.. versionchanged:: 21.1.0 *cmp* undeprecated
|
||||
.. versionadded:: 22.2.0 *alias*
|
||||
.. versionchanged:: 25.4.0
|
||||
*kw_only* can now be None, and its default is also changed from False to
|
||||
None.
|
||||
"""
|
||||
eq, eq_key, order, order_key = _determine_attrib_eq_order(
|
||||
cmp, eq, order, True
|
||||
@@ -373,7 +377,12 @@ def _collect_base_attrs_broken(cls, taken_attr_names):
|
||||
|
||||
|
||||
def _transform_attrs(
|
||||
cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer
|
||||
cls,
|
||||
these,
|
||||
auto_attribs,
|
||||
kw_only,
|
||||
collect_by_mro,
|
||||
field_transformer,
|
||||
) -> _Attributes:
|
||||
"""
|
||||
Transform all `_CountingAttr`s on a class into `Attribute`s.
|
||||
@@ -428,8 +437,15 @@ def _transform_attrs(
|
||||
)
|
||||
|
||||
fca = Attribute.from_counting_attr
|
||||
no = ClassProps.KeywordOnly.NO
|
||||
own_attrs = [
|
||||
fca(attr_name, ca, anns.get(attr_name)) for attr_name, ca in ca_list
|
||||
fca(
|
||||
attr_name,
|
||||
ca,
|
||||
kw_only is not no,
|
||||
anns.get(attr_name),
|
||||
)
|
||||
for attr_name, ca in ca_list
|
||||
]
|
||||
|
||||
if collect_by_mro:
|
||||
@@ -441,12 +457,21 @@ def _transform_attrs(
|
||||
cls, {a.name for a in own_attrs}
|
||||
)
|
||||
|
||||
if kw_only:
|
||||
if kw_only is ClassProps.KeywordOnly.FORCE:
|
||||
own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
|
||||
base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
|
||||
|
||||
attrs = base_attrs + own_attrs
|
||||
|
||||
# Resolve default field alias before executing field_transformer, so that
|
||||
# the transformer receives fully populated Attribute objects with usable
|
||||
# alias values.
|
||||
for a in attrs:
|
||||
if not a.alias:
|
||||
# Evolve is very slow, so we hold our nose and do it dirty.
|
||||
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
|
||||
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
|
||||
|
||||
if field_transformer is not None:
|
||||
attrs = tuple(field_transformer(cls, attrs))
|
||||
|
||||
@@ -464,13 +489,12 @@ def _transform_attrs(
|
||||
if had_default is False and a.default is not NOTHING:
|
||||
had_default = True
|
||||
|
||||
# Resolve default field alias after executing field_transformer.
|
||||
# This allows field_transformer to differentiate between explicit vs
|
||||
# default aliases and supply their own defaults.
|
||||
# Resolve default field alias for any new attributes that the
|
||||
# field_transformer may have added without setting an alias.
|
||||
for a in attrs:
|
||||
if not a.alias:
|
||||
# Evolve is very slow, so we hold our nose and do it dirty.
|
||||
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
|
||||
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
|
||||
|
||||
# Create AttrsClass *after* applying the field_transformer since it may
|
||||
# add or remove attributes!
|
||||
@@ -553,7 +577,7 @@ def _frozen_delattrs(self, name):
|
||||
"""
|
||||
Attached to frozen classes as __delattr__.
|
||||
"""
|
||||
if isinstance(self, BaseException) and name in ("__notes__",):
|
||||
if isinstance(self, BaseException) and name == "__notes__":
|
||||
BaseException.__delattr__(self, name)
|
||||
return
|
||||
|
||||
@@ -651,38 +675,31 @@ class _ClassBuilder:
|
||||
self,
|
||||
cls: type,
|
||||
these,
|
||||
slots,
|
||||
frozen,
|
||||
weakref_slot,
|
||||
getstate_setstate,
|
||||
auto_attribs,
|
||||
kw_only,
|
||||
cache_hash,
|
||||
is_exc,
|
||||
collect_by_mro,
|
||||
on_setattr,
|
||||
has_custom_setattr,
|
||||
field_transformer,
|
||||
auto_attribs: bool,
|
||||
props: ClassProps,
|
||||
has_custom_setattr: bool,
|
||||
):
|
||||
attrs, base_attrs, base_map = _transform_attrs(
|
||||
cls,
|
||||
these,
|
||||
auto_attribs,
|
||||
kw_only,
|
||||
collect_by_mro,
|
||||
field_transformer,
|
||||
props.kw_only,
|
||||
props.collected_fields_by_mro,
|
||||
props.field_transformer,
|
||||
)
|
||||
|
||||
self._cls = cls
|
||||
self._cls_dict = dict(cls.__dict__) if slots else {}
|
||||
self._cls_dict = dict(cls.__dict__) if props.is_slotted else {}
|
||||
self._attrs = attrs
|
||||
self._base_names = {a.name for a in base_attrs}
|
||||
self._base_attr_map = base_map
|
||||
self._attr_names = tuple(a.name for a in attrs)
|
||||
self._slots = slots
|
||||
self._frozen = frozen
|
||||
self._weakref_slot = weakref_slot
|
||||
self._cache_hash = cache_hash
|
||||
self._slots = props.is_slotted
|
||||
self._frozen = props.is_frozen
|
||||
self._weakref_slot = props.has_weakref_slot
|
||||
self._cache_hash = (
|
||||
props.hashability is ClassProps.Hashability.HASHABLE_CACHED
|
||||
)
|
||||
self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
|
||||
self._pre_init_has_args = False
|
||||
if self._has_pre_init:
|
||||
@@ -693,20 +710,21 @@ class _ClassBuilder:
|
||||
self._pre_init_has_args = len(pre_init_signature.parameters) > 1
|
||||
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
|
||||
self._delete_attribs = not bool(these)
|
||||
self._is_exc = is_exc
|
||||
self._on_setattr = on_setattr
|
||||
self._is_exc = props.is_exception
|
||||
self._on_setattr = props.on_setattr_hook
|
||||
|
||||
self._has_custom_setattr = has_custom_setattr
|
||||
self._wrote_own_setattr = False
|
||||
|
||||
self._cls_dict["__attrs_attrs__"] = self._attrs
|
||||
self._cls_dict["__attrs_props__"] = props
|
||||
|
||||
if frozen:
|
||||
if props.is_frozen:
|
||||
self._cls_dict["__setattr__"] = _frozen_setattrs
|
||||
self._cls_dict["__delattr__"] = _frozen_delattrs
|
||||
|
||||
self._wrote_own_setattr = True
|
||||
elif on_setattr in (
|
||||
elif self._on_setattr in (
|
||||
_DEFAULT_ON_SETATTR,
|
||||
setters.validate,
|
||||
setters.convert,
|
||||
@@ -722,18 +740,18 @@ class _ClassBuilder:
|
||||
break
|
||||
if (
|
||||
(
|
||||
on_setattr == _DEFAULT_ON_SETATTR
|
||||
self._on_setattr == _DEFAULT_ON_SETATTR
|
||||
and not (has_validator or has_converter)
|
||||
)
|
||||
or (on_setattr == setters.validate and not has_validator)
|
||||
or (on_setattr == setters.convert and not has_converter)
|
||||
or (self._on_setattr == setters.validate and not has_validator)
|
||||
or (self._on_setattr == setters.convert and not has_converter)
|
||||
):
|
||||
# If class-level on_setattr is set to convert + validate, but
|
||||
# there's no field to convert or validate, pretend like there's
|
||||
# no on_setattr.
|
||||
self._on_setattr = None
|
||||
|
||||
if getstate_setstate:
|
||||
if props.added_pickling:
|
||||
(
|
||||
self._cls_dict["__getstate__"],
|
||||
self._cls_dict["__setstate__"],
|
||||
@@ -784,6 +802,7 @@ class _ClassBuilder:
|
||||
self._eval_snippets()
|
||||
if self._slots is True:
|
||||
cls = self._create_slots_class()
|
||||
self._cls.__attrs_base_of_slotted__ = weakref.ref(cls)
|
||||
else:
|
||||
cls = self._patch_original_class()
|
||||
if PY_3_10_PLUS:
|
||||
@@ -845,6 +864,10 @@ class _ClassBuilder:
|
||||
if k not in (*tuple(self._attr_names), "__dict__", "__weakref__")
|
||||
}
|
||||
|
||||
# 3.14.0rc2+
|
||||
if hasattr(sys, "_clear_type_descriptors"):
|
||||
sys._clear_type_descriptors(self._cls)
|
||||
|
||||
# If our class doesn't have its own implementation of __setattr__
|
||||
# (either from the user or by us), check the bases, if one of them has
|
||||
# an attrs-made __setattr__, that needs to be reset. We don't walk the
|
||||
@@ -1081,9 +1104,7 @@ class _ClassBuilder:
|
||||
return self
|
||||
|
||||
def add_replace(self):
|
||||
self._cls_dict["__replace__"] = self._add_method_dunders(
|
||||
lambda self, **changes: evolve(self, **changes)
|
||||
)
|
||||
self._cls_dict["__replace__"] = self._add_method_dunders(evolve)
|
||||
return self
|
||||
|
||||
def add_match_args(self):
|
||||
@@ -1326,6 +1347,7 @@ def attrs(
|
||||
field_transformer=None,
|
||||
match_args=True,
|
||||
unsafe_hash=None,
|
||||
force_kw_only=True,
|
||||
):
|
||||
r"""
|
||||
A class decorator that adds :term:`dunder methods` according to the
|
||||
@@ -1392,6 +1414,10 @@ def attrs(
|
||||
If a class has an *inherited* classmethod called
|
||||
``__attrs_init_subclass__``, it is executed after the class is created.
|
||||
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
|
||||
.. versionchanged:: 25.4.0
|
||||
*kw_only* now only applies to attributes defined in the current class,
|
||||
and respects attribute-level ``kw_only=False`` settings.
|
||||
.. versionadded:: 25.4.0 *force_kw_only*
|
||||
"""
|
||||
if repr_ns is not None:
|
||||
import warnings
|
||||
@@ -1413,6 +1439,7 @@ def attrs(
|
||||
on_setattr = setters.pipe(*on_setattr)
|
||||
|
||||
def wrap(cls):
|
||||
nonlocal hash
|
||||
is_frozen = frozen or _has_frozen_base_class(cls)
|
||||
is_exc = auto_exc is True and issubclass(cls, BaseException)
|
||||
has_own_setattr = auto_detect and _has_own_attribute(
|
||||
@@ -1423,84 +1450,112 @@ def attrs(
|
||||
msg = "Can't freeze a class with a custom __setattr__."
|
||||
raise ValueError(msg)
|
||||
|
||||
builder = _ClassBuilder(
|
||||
cls,
|
||||
these,
|
||||
slots,
|
||||
is_frozen,
|
||||
weakref_slot,
|
||||
_determine_whether_to_implement(
|
||||
eq = not is_exc and _determine_whether_to_implement(
|
||||
cls, eq_, auto_detect, ("__eq__", "__ne__")
|
||||
)
|
||||
|
||||
Hashability = ClassProps.Hashability
|
||||
|
||||
if is_exc:
|
||||
hashability = Hashability.LEAVE_ALONE
|
||||
elif hash is True:
|
||||
hashability = (
|
||||
Hashability.HASHABLE_CACHED
|
||||
if cache_hash
|
||||
else Hashability.HASHABLE
|
||||
)
|
||||
elif hash is False:
|
||||
hashability = Hashability.LEAVE_ALONE
|
||||
elif hash is None:
|
||||
if auto_detect is True and _has_own_attribute(cls, "__hash__"):
|
||||
hashability = Hashability.LEAVE_ALONE
|
||||
elif eq is True and is_frozen is True:
|
||||
hashability = (
|
||||
Hashability.HASHABLE_CACHED
|
||||
if cache_hash
|
||||
else Hashability.HASHABLE
|
||||
)
|
||||
elif eq is False:
|
||||
hashability = Hashability.LEAVE_ALONE
|
||||
else:
|
||||
hashability = Hashability.UNHASHABLE
|
||||
else:
|
||||
msg = "Invalid value for hash. Must be True, False, or None."
|
||||
raise TypeError(msg)
|
||||
|
||||
KeywordOnly = ClassProps.KeywordOnly
|
||||
if kw_only:
|
||||
kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES
|
||||
else:
|
||||
kwo = KeywordOnly.NO
|
||||
|
||||
props = ClassProps(
|
||||
is_exception=is_exc,
|
||||
is_frozen=is_frozen,
|
||||
is_slotted=slots,
|
||||
collected_fields_by_mro=collect_by_mro,
|
||||
added_init=_determine_whether_to_implement(
|
||||
cls, init, auto_detect, ("__init__",)
|
||||
),
|
||||
added_repr=_determine_whether_to_implement(
|
||||
cls, repr, auto_detect, ("__repr__",)
|
||||
),
|
||||
added_eq=eq,
|
||||
added_ordering=not is_exc
|
||||
and _determine_whether_to_implement(
|
||||
cls,
|
||||
order_,
|
||||
auto_detect,
|
||||
("__lt__", "__le__", "__gt__", "__ge__"),
|
||||
),
|
||||
hashability=hashability,
|
||||
added_match_args=match_args,
|
||||
kw_only=kwo,
|
||||
has_weakref_slot=weakref_slot,
|
||||
added_str=str,
|
||||
added_pickling=_determine_whether_to_implement(
|
||||
cls,
|
||||
getstate_setstate,
|
||||
auto_detect,
|
||||
("__getstate__", "__setstate__"),
|
||||
default=slots,
|
||||
),
|
||||
auto_attribs,
|
||||
kw_only,
|
||||
cache_hash,
|
||||
is_exc,
|
||||
collect_by_mro,
|
||||
on_setattr,
|
||||
has_own_setattr,
|
||||
field_transformer,
|
||||
on_setattr_hook=on_setattr,
|
||||
field_transformer=field_transformer,
|
||||
)
|
||||
|
||||
if _determine_whether_to_implement(
|
||||
cls, repr, auto_detect, ("__repr__",)
|
||||
):
|
||||
if not props.is_hashable and cache_hash:
|
||||
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
|
||||
raise TypeError(msg)
|
||||
|
||||
builder = _ClassBuilder(
|
||||
cls,
|
||||
these,
|
||||
auto_attribs=auto_attribs,
|
||||
props=props,
|
||||
has_custom_setattr=has_own_setattr,
|
||||
)
|
||||
|
||||
if props.added_repr:
|
||||
builder.add_repr(repr_ns)
|
||||
|
||||
if str is True:
|
||||
if props.added_str:
|
||||
builder.add_str()
|
||||
|
||||
eq = _determine_whether_to_implement(
|
||||
cls, eq_, auto_detect, ("__eq__", "__ne__")
|
||||
)
|
||||
if not is_exc and eq is True:
|
||||
if props.added_eq:
|
||||
builder.add_eq()
|
||||
if not is_exc and _determine_whether_to_implement(
|
||||
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
|
||||
):
|
||||
if props.added_ordering:
|
||||
builder.add_order()
|
||||
|
||||
if not frozen:
|
||||
builder.add_setattr()
|
||||
|
||||
nonlocal hash
|
||||
if (
|
||||
hash is None
|
||||
and auto_detect is True
|
||||
and _has_own_attribute(cls, "__hash__")
|
||||
):
|
||||
hash = False
|
||||
|
||||
if hash is not True and hash is not False and hash is not None:
|
||||
# Can't use `hash in` because 1 == True for example.
|
||||
msg = "Invalid value for hash. Must be True, False, or None."
|
||||
raise TypeError(msg)
|
||||
|
||||
if hash is False or (hash is None and eq is False) or is_exc:
|
||||
# Don't do anything. Should fall back to __object__'s __hash__
|
||||
# which is by id.
|
||||
if cache_hash:
|
||||
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
|
||||
raise TypeError(msg)
|
||||
elif hash is True or (
|
||||
hash is None and eq is True and is_frozen is True
|
||||
):
|
||||
# Build a __hash__ if told so, or if it's safe.
|
||||
if props.is_hashable:
|
||||
builder.add_hash()
|
||||
else:
|
||||
# Raise TypeError on attempts to hash.
|
||||
if cache_hash:
|
||||
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
|
||||
raise TypeError(msg)
|
||||
elif props.hashability is Hashability.UNHASHABLE:
|
||||
builder.make_unhashable()
|
||||
|
||||
if _determine_whether_to_implement(
|
||||
cls, init, auto_detect, ("__init__",)
|
||||
):
|
||||
if props.added_init:
|
||||
builder.add_init()
|
||||
else:
|
||||
builder.add_attrs_init()
|
||||
@@ -1835,16 +1890,16 @@ def _add_repr(cls, ns=None, attrs=None):
|
||||
|
||||
def fields(cls):
|
||||
"""
|
||||
Return the tuple of *attrs* attributes for a class.
|
||||
Return the tuple of *attrs* attributes for a class or instance.
|
||||
|
||||
The tuple also allows accessing the fields by their names (see below for
|
||||
examples).
|
||||
|
||||
Args:
|
||||
cls (type): Class to introspect.
|
||||
cls (type): Class or instance to introspect.
|
||||
|
||||
Raises:
|
||||
TypeError: If *cls* is not a class.
|
||||
TypeError: If *cls* is neither a class nor an *attrs* instance.
|
||||
|
||||
attrs.exceptions.NotAnAttrsClassError:
|
||||
If *cls* is not an *attrs* class.
|
||||
@@ -1855,12 +1910,17 @@ def fields(cls):
|
||||
.. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
|
||||
by name.
|
||||
.. versionchanged:: 23.1.0 Add support for generic classes.
|
||||
.. versionchanged:: 26.1.0 Add support for instances.
|
||||
"""
|
||||
generic_base = get_generic_base(cls)
|
||||
|
||||
if generic_base is None and not isinstance(cls, type):
|
||||
msg = "Passed object must be a class."
|
||||
raise TypeError(msg)
|
||||
type_ = type(cls)
|
||||
if getattr(type_, "__attrs_attrs__", None) is None:
|
||||
msg = "Passed object must be a class or attrs instance."
|
||||
raise TypeError(msg)
|
||||
|
||||
return fields(type_)
|
||||
|
||||
attrs = getattr(cls, "__attrs_attrs__", None)
|
||||
|
||||
@@ -1967,7 +2027,7 @@ def _make_init_script(
|
||||
attr_dict[a.name] = a
|
||||
|
||||
if a.on_setattr is not None:
|
||||
if frozen is True:
|
||||
if frozen is True and a.on_setattr is not setters.NO_OP:
|
||||
msg = "Frozen classes can't use on_setattr."
|
||||
raise ValueError(msg)
|
||||
|
||||
@@ -2126,8 +2186,9 @@ def _attrs_to_init_script(
|
||||
)
|
||||
lines.extend(extra_lines)
|
||||
|
||||
args = []
|
||||
kw_only_args = []
|
||||
args = [] # Parameters in the definition of __init__
|
||||
pre_init_args = [] # Parameters in the call to __attrs_pre_init__
|
||||
kw_only_args = [] # Used for both 'args' and 'pre_init_args' above
|
||||
attrs_to_validate = []
|
||||
|
||||
# This is a dictionary of names to validator and converter callables.
|
||||
@@ -2205,6 +2266,7 @@ def _attrs_to_init_script(
|
||||
kw_only_args.append(arg)
|
||||
else:
|
||||
args.append(arg)
|
||||
pre_init_args.append(arg_name)
|
||||
|
||||
if converter is not None:
|
||||
lines.append(
|
||||
@@ -2224,6 +2286,7 @@ def _attrs_to_init_script(
|
||||
kw_only_args.append(arg)
|
||||
else:
|
||||
args.append(arg)
|
||||
pre_init_args.append(arg_name)
|
||||
lines.append(f"if {arg_name} is not NOTHING:")
|
||||
|
||||
init_factory_name = _INIT_FACTORY_PAT % (a.name,)
|
||||
@@ -2266,6 +2329,7 @@ def _attrs_to_init_script(
|
||||
kw_only_args.append(arg_name)
|
||||
else:
|
||||
args.append(arg_name)
|
||||
pre_init_args.append(arg_name)
|
||||
|
||||
if converter is not None:
|
||||
lines.append(
|
||||
@@ -2322,7 +2386,7 @@ def _attrs_to_init_script(
|
||||
lines.append(f"BaseException.__init__(self, {vals})")
|
||||
|
||||
args = ", ".join(args)
|
||||
pre_init_args = args
|
||||
pre_init_args = ", ".join(pre_init_args)
|
||||
if kw_only_args:
|
||||
# leading comma & kw_only args
|
||||
args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}"
|
||||
@@ -2337,7 +2401,7 @@ def _attrs_to_init_script(
|
||||
pre_init_args += pre_init_kw_only_args
|
||||
|
||||
if call_pre_init and pre_init_has_args:
|
||||
# If pre init method has arguments, pass same arguments as `__init__`.
|
||||
# If pre init method has arguments, pass the values given to __init__.
|
||||
lines[0] = f"self.__attrs_pre_init__({pre_init_args})"
|
||||
|
||||
# Python <3.12 doesn't allow backslashes in f-strings.
|
||||
@@ -2376,6 +2440,8 @@ class Attribute:
|
||||
- ``name`` (`str`): The name of the attribute.
|
||||
- ``alias`` (`str`): The __init__ parameter name of the attribute, after
|
||||
any explicit overrides and default private-attribute-name handling.
|
||||
- ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically
|
||||
generated (``True``) or explicitly provided by the user (``False``).
|
||||
- ``inherited`` (`bool`): Whether or not that attribute has been inherited
|
||||
from a base class.
|
||||
- ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The
|
||||
@@ -2401,6 +2467,7 @@ class Attribute:
|
||||
equality checks and hashing anymore.
|
||||
.. versionadded:: 21.1.0 *eq_key* and *order_key*
|
||||
.. versionadded:: 22.2.0 *alias*
|
||||
.. versionadded:: 26.1.0 *alias_is_default*
|
||||
|
||||
For the full version history of the fields, see `attr.ib`.
|
||||
"""
|
||||
@@ -2425,6 +2492,7 @@ class Attribute:
|
||||
"inherited",
|
||||
"on_setattr",
|
||||
"alias",
|
||||
"alias_is_default",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -2447,6 +2515,7 @@ class Attribute:
|
||||
order_key=None,
|
||||
on_setattr=None,
|
||||
alias=None,
|
||||
alias_is_default=None,
|
||||
):
|
||||
eq, eq_key, order, order_key = _determine_attrib_eq_order(
|
||||
cmp, eq_key or eq, order_key or order, True
|
||||
@@ -2481,12 +2550,20 @@ class Attribute:
|
||||
bound_setattr("inherited", inherited)
|
||||
bound_setattr("on_setattr", on_setattr)
|
||||
bound_setattr("alias", alias)
|
||||
bound_setattr(
|
||||
"alias_is_default",
|
||||
alias is None if alias_is_default is None else alias_is_default,
|
||||
)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
raise FrozenInstanceError
|
||||
|
||||
@classmethod
|
||||
def from_counting_attr(cls, name: str, ca: _CountingAttr, type=None):
|
||||
def from_counting_attr(
|
||||
cls, name: str, ca: _CountingAttr, kw_only: bool, type=None
|
||||
):
|
||||
# The 'kw_only' argument is the class-level setting, and is used if the
|
||||
# attribute itself does not explicitly set 'kw_only'.
|
||||
# type holds the annotated value. deal with conflicts:
|
||||
if type is None:
|
||||
type = ca.type
|
||||
@@ -2505,13 +2582,14 @@ class Attribute:
|
||||
ca.metadata,
|
||||
type,
|
||||
ca.converter,
|
||||
ca.kw_only,
|
||||
kw_only if ca.kw_only is None else ca.kw_only,
|
||||
ca.eq,
|
||||
ca.eq_key,
|
||||
ca.order,
|
||||
ca.order_key,
|
||||
ca.on_setattr,
|
||||
ca.alias,
|
||||
ca.alias is None,
|
||||
)
|
||||
|
||||
# Don't use attrs.evolve since fields(Attribute) doesn't work
|
||||
@@ -2530,6 +2608,20 @@ class Attribute:
|
||||
|
||||
new._setattrs(changes.items())
|
||||
|
||||
if "alias" in changes and "alias_is_default" not in changes:
|
||||
# Explicit alias provided -- no longer the default.
|
||||
_OBJ_SETATTR.__get__(new)("alias_is_default", False)
|
||||
elif (
|
||||
"name" in changes
|
||||
and "alias" not in changes
|
||||
# Don't auto-generate alias if the user picked picked the old one.
|
||||
and self.alias_is_default
|
||||
):
|
||||
# Name changed, alias was auto-generated -- update it.
|
||||
_OBJ_SETATTR.__get__(new)(
|
||||
"alias", _default_init_alias_for(new.name)
|
||||
)
|
||||
|
||||
return new
|
||||
|
||||
# Don't use _add_pickle since fields(Attribute) doesn't work
|
||||
@@ -2546,6 +2638,17 @@ class Attribute:
|
||||
"""
|
||||
Play nice with pickle.
|
||||
"""
|
||||
if len(state) < len(self.__slots__):
|
||||
# Pre-26.1.0 pickle without alias_is_default -- infer it
|
||||
# heuristically.
|
||||
state_dict = dict(zip(self.__slots__, state))
|
||||
alias_is_default = state_dict.get(
|
||||
"alias"
|
||||
) is None or state_dict.get("alias") == _default_init_alias_for(
|
||||
state_dict["name"]
|
||||
)
|
||||
state = (*state, alias_is_default)
|
||||
|
||||
self._setattrs(zip(self.__slots__, state))
|
||||
|
||||
def _setattrs(self, name_values_pairs):
|
||||
@@ -2569,7 +2672,7 @@ _a = [
|
||||
name=name,
|
||||
default=NOTHING,
|
||||
validator=None,
|
||||
repr=True,
|
||||
repr=(name != "alias_is_default"),
|
||||
cmp=None,
|
||||
eq=True,
|
||||
order=False,
|
||||
@@ -2741,6 +2844,188 @@ class _CountingAttr:
|
||||
_CountingAttr = _add_eq(_add_repr(_CountingAttr))
|
||||
|
||||
|
||||
class ClassProps:
|
||||
"""
|
||||
Effective class properties as derived from parameters to `attr.s()` or
|
||||
`define()` decorators.
|
||||
|
||||
This is the same data structure that *attrs* uses internally to decide how
|
||||
to construct the final class.
|
||||
|
||||
Warning:
|
||||
|
||||
This feature is currently **experimental** and is not covered by our
|
||||
strict backwards-compatibility guarantees.
|
||||
|
||||
|
||||
Attributes:
|
||||
is_exception (bool):
|
||||
Whether the class is treated as an exception class.
|
||||
|
||||
is_slotted (bool):
|
||||
Whether the class is `slotted <slotted classes>`.
|
||||
|
||||
has_weakref_slot (bool):
|
||||
Whether the class has a slot for weak references.
|
||||
|
||||
is_frozen (bool):
|
||||
Whether the class is frozen.
|
||||
|
||||
kw_only (KeywordOnly):
|
||||
Whether / how the class enforces keyword-only arguments on the
|
||||
``__init__`` method.
|
||||
|
||||
collected_fields_by_mro (bool):
|
||||
Whether the class fields were collected by method resolution order.
|
||||
That is, correctly but unlike `dataclasses`.
|
||||
|
||||
added_init (bool):
|
||||
Whether the class has an *attrs*-generated ``__init__`` method.
|
||||
|
||||
added_repr (bool):
|
||||
Whether the class has an *attrs*-generated ``__repr__`` method.
|
||||
|
||||
added_eq (bool):
|
||||
Whether the class has *attrs*-generated equality methods.
|
||||
|
||||
added_ordering (bool):
|
||||
Whether the class has *attrs*-generated ordering methods.
|
||||
|
||||
hashability (Hashability): How `hashable <hashing>` the class is.
|
||||
|
||||
added_match_args (bool):
|
||||
Whether the class supports positional `match <match>` over its
|
||||
fields.
|
||||
|
||||
added_str (bool):
|
||||
Whether the class has an *attrs*-generated ``__str__`` method.
|
||||
|
||||
added_pickling (bool):
|
||||
Whether the class has *attrs*-generated ``__getstate__`` and
|
||||
``__setstate__`` methods for `pickle`.
|
||||
|
||||
on_setattr_hook (Callable[[Any, Attribute[Any], Any], Any] | None):
|
||||
The class's ``__setattr__`` hook.
|
||||
|
||||
field_transformer (Callable[[Attribute[Any]], Attribute[Any]] | None):
|
||||
The class's `field transformers <transform-fields>`.
|
||||
|
||||
.. versionadded:: 25.4.0
|
||||
"""
|
||||
|
||||
class Hashability(enum.Enum):
|
||||
"""
|
||||
The hashability of a class.
|
||||
|
||||
.. versionadded:: 25.4.0
|
||||
"""
|
||||
|
||||
HASHABLE = "hashable"
|
||||
"""Write a ``__hash__``."""
|
||||
HASHABLE_CACHED = "hashable_cache"
|
||||
"""Write a ``__hash__`` and cache the hash."""
|
||||
UNHASHABLE = "unhashable"
|
||||
"""Set ``__hash__`` to ``None``."""
|
||||
LEAVE_ALONE = "leave_alone"
|
||||
"""Don't touch ``__hash__``."""
|
||||
|
||||
class KeywordOnly(enum.Enum):
|
||||
"""
|
||||
How attributes should be treated regarding keyword-only parameters.
|
||||
|
||||
.. versionadded:: 25.4.0
|
||||
"""
|
||||
|
||||
NO = "no"
|
||||
"""Attributes are not keyword-only."""
|
||||
YES = "yes"
|
||||
"""Attributes in current class without kw_only=False are keyword-only."""
|
||||
FORCE = "force"
|
||||
"""All attributes are keyword-only."""
|
||||
|
||||
__slots__ = ( # noqa: RUF023 -- order matters for __init__
|
||||
"is_exception",
|
||||
"is_slotted",
|
||||
"has_weakref_slot",
|
||||
"is_frozen",
|
||||
"kw_only",
|
||||
"collected_fields_by_mro",
|
||||
"added_init",
|
||||
"added_repr",
|
||||
"added_eq",
|
||||
"added_ordering",
|
||||
"hashability",
|
||||
"added_match_args",
|
||||
"added_str",
|
||||
"added_pickling",
|
||||
"on_setattr_hook",
|
||||
"field_transformer",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_exception,
|
||||
is_slotted,
|
||||
has_weakref_slot,
|
||||
is_frozen,
|
||||
kw_only,
|
||||
collected_fields_by_mro,
|
||||
added_init,
|
||||
added_repr,
|
||||
added_eq,
|
||||
added_ordering,
|
||||
hashability,
|
||||
added_match_args,
|
||||
added_str,
|
||||
added_pickling,
|
||||
on_setattr_hook,
|
||||
field_transformer,
|
||||
):
|
||||
self.is_exception = is_exception
|
||||
self.is_slotted = is_slotted
|
||||
self.has_weakref_slot = has_weakref_slot
|
||||
self.is_frozen = is_frozen
|
||||
self.kw_only = kw_only
|
||||
self.collected_fields_by_mro = collected_fields_by_mro
|
||||
self.added_init = added_init
|
||||
self.added_repr = added_repr
|
||||
self.added_eq = added_eq
|
||||
self.added_ordering = added_ordering
|
||||
self.hashability = hashability
|
||||
self.added_match_args = added_match_args
|
||||
self.added_str = added_str
|
||||
self.added_pickling = added_pickling
|
||||
self.on_setattr_hook = on_setattr_hook
|
||||
self.field_transformer = field_transformer
|
||||
|
||||
@property
|
||||
def is_hashable(self):
|
||||
return (
|
||||
self.hashability is ClassProps.Hashability.HASHABLE
|
||||
or self.hashability is ClassProps.Hashability.HASHABLE_CACHED
|
||||
)
|
||||
|
||||
|
||||
_cas = [
|
||||
Attribute(
|
||||
name=name,
|
||||
default=NOTHING,
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=None,
|
||||
eq=True,
|
||||
order=False,
|
||||
hash=True,
|
||||
init=True,
|
||||
inherited=False,
|
||||
alias=_default_init_alias_for(name),
|
||||
)
|
||||
for name in ClassProps.__slots__
|
||||
]
|
||||
|
||||
ClassProps = _add_eq(_add_repr(ClassProps, attrs=_cas), attrs=_cas)
|
||||
|
||||
|
||||
class Factory:
|
||||
"""
|
||||
Stores a factory callable.
|
||||
@@ -2848,9 +3133,7 @@ class Converter:
|
||||
value, field
|
||||
)
|
||||
else:
|
||||
self.__call__ = lambda value, instance, field: self.converter(
|
||||
value, instance, field
|
||||
)
|
||||
self.__call__ = self.converter
|
||||
|
||||
rt = ex.get_return_type()
|
||||
if rt is not None:
|
||||
|
||||
@@ -17,7 +17,7 @@ from ._make import (
|
||||
attrib,
|
||||
attrs,
|
||||
)
|
||||
from .exceptions import UnannotatedAttributeError
|
||||
from .exceptions import NotAnAttrsClassError, UnannotatedAttributeError
|
||||
|
||||
|
||||
def define(
|
||||
@@ -43,6 +43,7 @@ def define(
|
||||
on_setattr=None,
|
||||
field_transformer=None,
|
||||
match_args=True,
|
||||
force_kw_only=False,
|
||||
):
|
||||
r"""
|
||||
A class decorator that adds :term:`dunder methods` according to
|
||||
@@ -76,7 +77,7 @@ def define(
|
||||
sensible ``__ne__`` by default, so it *should* be enough to only
|
||||
implement ``__eq__`` in most cases).
|
||||
|
||||
Passing True or False` to *init*, *repr*, *eq*, or *hash*
|
||||
Passing :data:`True` or :data:`False` to *init*, *repr*, *eq*, or *hash*
|
||||
overrides whatever *auto_detect* would determine.
|
||||
|
||||
auto_exc (bool):
|
||||
@@ -214,8 +215,12 @@ def define(
|
||||
5. Subclasses of a frozen class are frozen too.
|
||||
|
||||
kw_only (bool):
|
||||
Make all attributes keyword-only in the generated ``__init__`` (if
|
||||
*init* is False, this parameter is ignored).
|
||||
Make attributes keyword-only in the generated ``__init__`` (if
|
||||
*init* is False, this parameter is ignored). Attributes that
|
||||
explicitly set ``kw_only=False`` are not affected; base class
|
||||
attributes are also not affected.
|
||||
|
||||
Also see *force_kw_only*.
|
||||
|
||||
weakref_slot (bool):
|
||||
Make instances weak-referenceable. This has no effect unless
|
||||
@@ -244,6 +249,15 @@ def define(
|
||||
See also `issue #428
|
||||
<https://github.com/python-attrs/attrs/issues/428>`_.
|
||||
|
||||
force_kw_only (bool):
|
||||
A back-compat flag for restoring pre-25.4.0 behavior. If True and
|
||||
``kw_only=True``, all attributes are made keyword-only, including
|
||||
base class attributes, and those set to ``kw_only=False`` at the
|
||||
attribute level. Defaults to False.
|
||||
|
||||
See also `issue #980
|
||||
<https://github.com/python-attrs/attrs/issues/980>`_.
|
||||
|
||||
getstate_setstate (bool | None):
|
||||
.. note::
|
||||
|
||||
@@ -319,6 +333,11 @@ def define(
|
||||
.. versionadded:: 24.3.0
|
||||
Unless already present, a ``__replace__`` method is automatically
|
||||
created for `copy.replace` (Python 3.13+ only).
|
||||
.. versionchanged:: 25.4.0
|
||||
*kw_only* now only applies to attributes defined in the current class,
|
||||
and respects attribute-level ``kw_only=False`` settings.
|
||||
.. versionadded:: 25.4.0
|
||||
Added *force_kw_only* to go back to the previous *kw_only* behavior.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -337,6 +356,7 @@ def define(
|
||||
- *auto_exc=True*
|
||||
- *auto_detect=True*
|
||||
- *order=False*
|
||||
- *force_kw_only=False*
|
||||
- Some options that were only relevant on Python 2 or were kept around
|
||||
for backwards-compatibility have been removed.
|
||||
|
||||
@@ -366,6 +386,7 @@ def define(
|
||||
on_setattr=on_setattr,
|
||||
field_transformer=field_transformer,
|
||||
match_args=match_args,
|
||||
force_kw_only=force_kw_only,
|
||||
)
|
||||
|
||||
def wrap(cls):
|
||||
@@ -424,7 +445,7 @@ def field(
|
||||
type=None,
|
||||
converter=None,
|
||||
factory=None,
|
||||
kw_only=False,
|
||||
kw_only=None,
|
||||
eq=None,
|
||||
order=None,
|
||||
on_setattr=None,
|
||||
@@ -550,9 +571,10 @@ def field(
|
||||
itself. You can use it as part of your own code or for `static type
|
||||
checking <types>`.
|
||||
|
||||
kw_only (bool):
|
||||
kw_only (bool | None):
|
||||
Make this attribute keyword-only in the generated ``__init__`` (if
|
||||
``init`` is False, this parameter is ignored).
|
||||
*init* is False, this parameter is ignored). If None (default),
|
||||
mirror the setting from `attrs.define`.
|
||||
|
||||
on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]):
|
||||
Allows to overwrite the *on_setattr* setting from `attr.s`. If left
|
||||
@@ -572,6 +594,9 @@ def field(
|
||||
.. versionadded:: 23.1.0
|
||||
The *type* parameter has been re-added; mostly for `attrs.make_class`.
|
||||
Please note that type checkers ignore this metadata.
|
||||
.. versionchanged:: 25.4.0
|
||||
*kw_only* can now be None, and its default is also changed from False to
|
||||
None.
|
||||
|
||||
.. seealso::
|
||||
|
||||
@@ -621,3 +646,29 @@ def astuple(inst, *, recurse=True, filter=None):
|
||||
return _astuple(
|
||||
inst=inst, recurse=recurse, filter=filter, retain_collection_types=True
|
||||
)
|
||||
|
||||
|
||||
def inspect(cls):
|
||||
"""
|
||||
Inspect the class and return its effective build parameters.
|
||||
|
||||
Warning:
|
||||
This feature is currently **experimental** and is not covered by our
|
||||
strict backwards-compatibility guarantees.
|
||||
|
||||
Args:
|
||||
cls: The *attrs*-decorated class to inspect.
|
||||
|
||||
Returns:
|
||||
The effective build parameters of the class.
|
||||
|
||||
Raises:
|
||||
NotAnAttrsClassError: If the class is not an *attrs*-decorated class.
|
||||
|
||||
.. versionadded:: 25.4.0
|
||||
"""
|
||||
try:
|
||||
return cls.__dict__["__attrs_props__"]
|
||||
except KeyError:
|
||||
msg = f"{cls!r} is not an attrs-decorated class."
|
||||
raise NotAnAttrsClassError(msg) from None
|
||||
|
||||
@@ -84,3 +84,6 @@ class VersionInfo:
|
||||
# Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
|
||||
# have to do anything special with releaselevel for now.
|
||||
return us < them
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.year, self.minor, self.micro, self.releaselevel))
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
|
||||
class FrozenError(AttributeError):
|
||||
"""
|
||||
@@ -16,8 +14,10 @@ class FrozenError(AttributeError):
|
||||
.. versionadded:: 20.1.0
|
||||
"""
|
||||
|
||||
msg = "can't set attribute"
|
||||
args: ClassVar[tuple[str]] = [msg]
|
||||
def __init__(self):
|
||||
msg = "can't set attribute"
|
||||
super().__init__(msg)
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class FrozenInstanceError(FrozenError):
|
||||
|
||||
@@ -79,12 +79,14 @@ def disabled():
|
||||
This context manager is not thread-safe!
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
.. versionchanged:: 26.1.0 The contextmanager is nestable.
|
||||
"""
|
||||
prev = get_run_validators()
|
||||
set_run_validators(False)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
set_run_validators(True)
|
||||
set_run_validators(prev)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
@@ -361,26 +363,32 @@ def deep_iterable(member_validator, iterable_validator=None):
|
||||
A validator that performs deep validation of an iterable.
|
||||
|
||||
Args:
|
||||
member_validator: Validator to apply to iterable members.
|
||||
member_validator: Validator(s) to apply to iterable members.
|
||||
|
||||
iterable_validator:
|
||||
Validator to apply to iterable itself (optional).
|
||||
Validator(s) to apply to iterable itself (optional).
|
||||
|
||||
Raises
|
||||
TypeError: if any sub-validators fail
|
||||
|
||||
.. versionadded:: 19.1.0
|
||||
|
||||
.. versionchanged:: 25.4.0
|
||||
*member_validator* and *iterable_validator* can now be a list or tuple
|
||||
of validators.
|
||||
"""
|
||||
if isinstance(member_validator, (list, tuple)):
|
||||
member_validator = and_(*member_validator)
|
||||
if isinstance(iterable_validator, (list, tuple)):
|
||||
iterable_validator = and_(*iterable_validator)
|
||||
return _DeepIterable(member_validator, iterable_validator)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
class _DeepMapping:
|
||||
key_validator = attrib(validator=is_callable())
|
||||
value_validator = attrib(validator=is_callable())
|
||||
mapping_validator = attrib(default=None, validator=optional(is_callable()))
|
||||
key_validator = attrib(validator=optional(is_callable()))
|
||||
value_validator = attrib(validator=optional(is_callable()))
|
||||
mapping_validator = attrib(validator=optional(is_callable()))
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
@@ -390,30 +398,62 @@ class _DeepMapping:
|
||||
self.mapping_validator(inst, attr, value)
|
||||
|
||||
for key in value:
|
||||
self.key_validator(inst, attr, key)
|
||||
self.value_validator(inst, attr, value[key])
|
||||
if self.key_validator is not None:
|
||||
self.key_validator(inst, attr, key)
|
||||
if self.value_validator is not None:
|
||||
self.value_validator(inst, attr, value[key])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
|
||||
|
||||
|
||||
def deep_mapping(key_validator, value_validator, mapping_validator=None):
|
||||
def deep_mapping(
|
||||
key_validator=None, value_validator=None, mapping_validator=None
|
||||
):
|
||||
"""
|
||||
A validator that performs deep validation of a dictionary.
|
||||
|
||||
Args:
|
||||
key_validator: Validator to apply to dictionary keys.
|
||||
All validators are optional, but at least one of *key_validator* or
|
||||
*value_validator* must be provided.
|
||||
|
||||
value_validator: Validator to apply to dictionary values.
|
||||
Args:
|
||||
key_validator: Validator(s) to apply to dictionary keys.
|
||||
|
||||
value_validator: Validator(s) to apply to dictionary values.
|
||||
|
||||
mapping_validator:
|
||||
Validator to apply to top-level mapping attribute (optional).
|
||||
Validator(s) to apply to top-level mapping attribute.
|
||||
|
||||
.. versionadded:: 19.1.0
|
||||
|
||||
.. versionchanged:: 25.4.0
|
||||
*key_validator* and *value_validator* are now optional, but at least one
|
||||
of them must be provided.
|
||||
|
||||
.. versionchanged:: 25.4.0
|
||||
*key_validator*, *value_validator*, and *mapping_validator* can now be a
|
||||
list or tuple of validators.
|
||||
|
||||
Raises:
|
||||
TypeError: if any sub-validators fail
|
||||
TypeError: If any sub-validator fails on validation.
|
||||
|
||||
ValueError:
|
||||
If neither *key_validator* nor *value_validator* is provided on
|
||||
instantiation.
|
||||
"""
|
||||
if key_validator is None and value_validator is None:
|
||||
msg = (
|
||||
"At least one of key_validator or value_validator must be provided"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
if isinstance(key_validator, (list, tuple)):
|
||||
key_validator = and_(*key_validator)
|
||||
if isinstance(value_validator, (list, tuple)):
|
||||
value_validator = and_(*value_validator)
|
||||
if isinstance(mapping_validator, (list, tuple)):
|
||||
mapping_validator = and_(*mapping_validator)
|
||||
|
||||
return _DeepMapping(key_validator, value_validator, mapping_validator)
|
||||
|
||||
|
||||
@@ -485,7 +525,7 @@ def gt(val):
|
||||
A validator that raises `ValueError` if the initializer is called with a
|
||||
number smaller or equal to *val*.
|
||||
|
||||
The validator uses `operator.ge` to compare the values.
|
||||
The validator uses `operator.gt` to compare the values.
|
||||
|
||||
Args:
|
||||
val: Exclusive lower bound for values
|
||||
|
||||
@@ -20,6 +20,9 @@ _T = TypeVar("_T")
|
||||
_T1 = TypeVar("_T1")
|
||||
_T2 = TypeVar("_T2")
|
||||
_T3 = TypeVar("_T3")
|
||||
_T4 = TypeVar("_T4")
|
||||
_T5 = TypeVar("_T5")
|
||||
_T6 = TypeVar("_T6")
|
||||
_I = TypeVar("_I", bound=Iterable)
|
||||
_K = TypeVar("_K")
|
||||
_V = TypeVar("_V")
|
||||
@@ -51,7 +54,7 @@ def optional(
|
||||
validator: (
|
||||
_ValidatorType[_T]
|
||||
| list[_ValidatorType[_T]]
|
||||
| tuple[_ValidatorType[_T]]
|
||||
| tuple[_ValidatorType[_T], ...]
|
||||
),
|
||||
) -> _ValidatorType[_T | None]: ...
|
||||
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
|
||||
@@ -63,12 +66,19 @@ def matches_re(
|
||||
) -> _ValidatorType[AnyStr]: ...
|
||||
def deep_iterable(
|
||||
member_validator: _ValidatorArgType[_T],
|
||||
iterable_validator: _ValidatorType[_I] | None = ...,
|
||||
iterable_validator: _ValidatorArgType[_I] | None = ...,
|
||||
) -> _ValidatorType[_I]: ...
|
||||
@overload
|
||||
def deep_mapping(
|
||||
key_validator: _ValidatorType[_K],
|
||||
value_validator: _ValidatorType[_V],
|
||||
mapping_validator: _ValidatorType[_M] | None = ...,
|
||||
key_validator: _ValidatorArgType[_K],
|
||||
value_validator: _ValidatorArgType[_V] | None = ...,
|
||||
mapping_validator: _ValidatorArgType[_M] | None = ...,
|
||||
) -> _ValidatorType[_M]: ...
|
||||
@overload
|
||||
def deep_mapping(
|
||||
key_validator: _ValidatorArgType[_K] | None = ...,
|
||||
value_validator: _ValidatorArgType[_V] = ...,
|
||||
mapping_validator: _ValidatorArgType[_M] | None = ...,
|
||||
) -> _ValidatorType[_M]: ...
|
||||
def is_callable() -> _ValidatorType[_T]: ...
|
||||
def lt(val: _T) -> _ValidatorType[_T]: ...
|
||||
@@ -83,4 +93,48 @@ def not_(
|
||||
msg: str | None = None,
|
||||
exc_types: type[Exception] | Iterable[type[Exception]] = ...,
|
||||
) -> _ValidatorType[_T]: ...
|
||||
def or_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
|
||||
@overload
|
||||
def or_(
|
||||
__v1: _ValidatorType[_T1],
|
||||
__v2: _ValidatorType[_T2],
|
||||
) -> _ValidatorType[_T1 | _T2]: ...
|
||||
@overload
|
||||
def or_(
|
||||
__v1: _ValidatorType[_T1],
|
||||
__v2: _ValidatorType[_T2],
|
||||
__v3: _ValidatorType[_T3],
|
||||
) -> _ValidatorType[_T1 | _T2 | _T3]: ...
|
||||
@overload
|
||||
def or_(
|
||||
__v1: _ValidatorType[_T1],
|
||||
__v2: _ValidatorType[_T2],
|
||||
__v3: _ValidatorType[_T3],
|
||||
__v4: _ValidatorType[_T4],
|
||||
) -> _ValidatorType[_T1 | _T2 | _T3 | _T4]: ...
|
||||
@overload
|
||||
def or_(
|
||||
__v1: _ValidatorType[_T1],
|
||||
__v2: _ValidatorType[_T2],
|
||||
__v3: _ValidatorType[_T3],
|
||||
__v4: _ValidatorType[_T4],
|
||||
__v5: _ValidatorType[_T5],
|
||||
) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5]: ...
|
||||
@overload
|
||||
def or_(
|
||||
__v1: _ValidatorType[_T1],
|
||||
__v2: _ValidatorType[_T2],
|
||||
__v3: _ValidatorType[_T3],
|
||||
__v4: _ValidatorType[_T4],
|
||||
__v5: _ValidatorType[_T5],
|
||||
__v6: _ValidatorType[_T6],
|
||||
) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5 | _T6]: ...
|
||||
@overload
|
||||
def or_(
|
||||
__v1: _ValidatorType[Any],
|
||||
__v2: _ValidatorType[Any],
|
||||
__v3: _ValidatorType[Any],
|
||||
__v4: _ValidatorType[Any],
|
||||
__v5: _ValidatorType[Any],
|
||||
__v6: _ValidatorType[Any],
|
||||
*validators: _ValidatorType[Any],
|
||||
) -> _ValidatorType[Any]: ...
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pip
|
||||
@@ -1,56 +0,0 @@
|
||||
attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057
|
||||
attr/__init__.pyi,sha256=QIXnnHPoucmDWkbpNsWTP-cgJ1bn8le7DjyRa_wYdew,11281
|
||||
attr/__pycache__/__init__.cpython-311.pyc,,
|
||||
attr/__pycache__/_cmp.cpython-311.pyc,,
|
||||
attr/__pycache__/_compat.cpython-311.pyc,,
|
||||
attr/__pycache__/_config.cpython-311.pyc,,
|
||||
attr/__pycache__/_funcs.cpython-311.pyc,,
|
||||
attr/__pycache__/_make.cpython-311.pyc,,
|
||||
attr/__pycache__/_next_gen.cpython-311.pyc,,
|
||||
attr/__pycache__/_version_info.cpython-311.pyc,,
|
||||
attr/__pycache__/converters.cpython-311.pyc,,
|
||||
attr/__pycache__/exceptions.cpython-311.pyc,,
|
||||
attr/__pycache__/filters.cpython-311.pyc,,
|
||||
attr/__pycache__/setters.cpython-311.pyc,,
|
||||
attr/__pycache__/validators.cpython-311.pyc,,
|
||||
attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117
|
||||
attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368
|
||||
attr/_compat.py,sha256=4hlXbWhdDjQCDK6FKF1EgnZ3POiHgtpp54qE0nxaGHg,2704
|
||||
attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843
|
||||
attr/_funcs.py,sha256=5-tUKJtp3h5El55EcDl6GWXFp68fT8D8U7uCRN6497I,15854
|
||||
attr/_make.py,sha256=lBUPPmxiA1BeHzB6OlHoCEh--tVvM1ozXO8eXOa6g4c,96664
|
||||
attr/_next_gen.py,sha256=7FRkbtl_N017SuBhf_Vw3mw2c2pGZhtCGOzadgz7tp4,24395
|
||||
attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469
|
||||
attr/_version_info.py,sha256=exSqb3b5E-fMSsgZAlEw9XcLpEgobPORCZpcaEglAM4,2121
|
||||
attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209
|
||||
attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861
|
||||
attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643
|
||||
attr/exceptions.py,sha256=HRFq4iybmv7-DcZwyjl6M1euM2YeJVK_hFxuaBGAngI,1977
|
||||
attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539
|
||||
attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795
|
||||
attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208
|
||||
attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617
|
||||
attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584
|
||||
attr/validators.py,sha256=WaB1HLAHHqRHWsrv_K9H-sJ7ESil3H3Cmv2d8TtVZx4,20046
|
||||
attr/validators.pyi,sha256=s2WhKPqskxbsckJfKk8zOuuB088GfgpyxcCYSNFLqNU,2603
|
||||
attrs-25.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
attrs-25.3.0.dist-info/METADATA,sha256=W38cREj7s1wqNf1fg4hVwZmL1xh0AdSp4IhtTMROinw,10993
|
||||
attrs-25.3.0.dist-info/RECORD,,
|
||||
attrs-25.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
attrs-25.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
||||
attrs-25.3.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109
|
||||
attrs/__init__.py,sha256=qeQJZ4O08yczSn840v9bYOaZyRE81WsVi-QCrY3krCU,1107
|
||||
attrs/__init__.pyi,sha256=nZmInocjM7tHV4AQw0vxO_fo6oJjL_PonlV9zKKW8DY,7931
|
||||
attrs/__pycache__/__init__.cpython-311.pyc,,
|
||||
attrs/__pycache__/converters.cpython-311.pyc,,
|
||||
attrs/__pycache__/exceptions.cpython-311.pyc,,
|
||||
attrs/__pycache__/filters.cpython-311.pyc,,
|
||||
attrs/__pycache__/setters.cpython-311.pyc,,
|
||||
attrs/__pycache__/validators.cpython-311.pyc,,
|
||||
attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76
|
||||
attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76
|
||||
attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73
|
||||
attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73
|
||||
attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76
|
||||
@@ -0,0 +1 @@
|
||||
uv
|
||||
+27
-60
@@ -1,6 +1,6 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: attrs
|
||||
Version: 25.3.0
|
||||
Version: 26.1.0
|
||||
Summary: Classes Without Boilerplate
|
||||
Project-URL: Documentation, https://www.attrs.org/
|
||||
Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html
|
||||
@@ -12,62 +12,16 @@ License-Expression: MIT
|
||||
License-File: LICENSE
|
||||
Keywords: attribute,boilerplate,class
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
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: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.8
|
||||
Provides-Extra: benchmark
|
||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'benchmark'
|
||||
Requires-Dist: hypothesis; extra == 'benchmark'
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'benchmark'
|
||||
Requires-Dist: pympler; extra == 'benchmark'
|
||||
Requires-Dist: pytest-codspeed; extra == 'benchmark'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'benchmark'
|
||||
Requires-Dist: pytest-xdist[psutil]; extra == 'benchmark'
|
||||
Requires-Dist: pytest>=4.3.0; extra == 'benchmark'
|
||||
Provides-Extra: cov
|
||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'cov'
|
||||
Requires-Dist: coverage[toml]>=5.3; extra == 'cov'
|
||||
Requires-Dist: hypothesis; extra == 'cov'
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'cov'
|
||||
Requires-Dist: pympler; extra == 'cov'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'cov'
|
||||
Requires-Dist: pytest-xdist[psutil]; extra == 'cov'
|
||||
Requires-Dist: pytest>=4.3.0; extra == 'cov'
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'dev'
|
||||
Requires-Dist: hypothesis; extra == 'dev'
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'dev'
|
||||
Requires-Dist: pre-commit-uv; extra == 'dev'
|
||||
Requires-Dist: pympler; extra == 'dev'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'dev'
|
||||
Requires-Dist: pytest-xdist[psutil]; extra == 'dev'
|
||||
Requires-Dist: pytest>=4.3.0; extra == 'dev'
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: cogapp; extra == 'docs'
|
||||
Requires-Dist: furo; extra == 'docs'
|
||||
Requires-Dist: myst-parser; extra == 'docs'
|
||||
Requires-Dist: sphinx; extra == 'docs'
|
||||
Requires-Dist: sphinx-notfound-page; extra == 'docs'
|
||||
Requires-Dist: sphinxcontrib-towncrier; extra == 'docs'
|
||||
Requires-Dist: towncrier; extra == 'docs'
|
||||
Provides-Extra: tests
|
||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'tests'
|
||||
Requires-Dist: hypothesis; extra == 'tests'
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests'
|
||||
Requires-Dist: pympler; extra == 'tests'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests'
|
||||
Requires-Dist: pytest-xdist[psutil]; extra == 'tests'
|
||||
Requires-Dist: pytest>=4.3.0; extra == 'tests'
|
||||
Provides-Extra: tests-mypy
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests-mypy'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests-mypy'
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
<p align="center">
|
||||
@@ -78,7 +32,7 @@ Description-Content-Type: text/markdown
|
||||
|
||||
|
||||
*attrs* is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka [dunder methods](https://www.attrs.org/en/latest/glossary.html#term-dunder-methods)).
|
||||
[Trusted by NASA](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/personalizing-your-profile#list-of-qualifying-repositories-for-mars-2020-helicopter-contributor-achievement) for Mars missions since 2020!
|
||||
Trusted by NASA for [Mars missions since 2020](https://github.com/readme/featured/nasa-ingenuity-helicopter)!
|
||||
|
||||
Its main goal is to help you to write **concise** and **correct** software without slowing down your code.
|
||||
|
||||
@@ -96,15 +50,15 @@ Especially those generously supporting us at the *The Organization* tier and hig
|
||||
import pathlib, tomllib
|
||||
|
||||
for sponsor in tomllib.loads(pathlib.Path("pyproject.toml").read_text())["tool"]["sponcon"]["sponsors"]:
|
||||
print(f'<a href="{sponsor["url"]}"><img title="{sponsor["title"]}" src="https://www.attrs.org/en/25.3.0/_static/sponsors/{sponsor["img"]}" width="190" /></a>')
|
||||
print(f'<a href="{sponsor["url"]}"><img title="{sponsor["title"]}" src="https://www.attrs.org/en/26.1.0/_static/sponsors/{sponsor["img"]}" width="190" /></a>')
|
||||
]]] -->
|
||||
<a href="https://www.variomedia.de/"><img title="Variomedia AG" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Variomedia.svg" width="190" /></a>
|
||||
<a href="https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek"><img title="Tidelift" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Tidelift.svg" width="190" /></a>
|
||||
<a href="https://klaviyo.com/"><img title="Klaviyo" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Klaviyo.svg" width="190" /></a>
|
||||
<a href="https://privacy-solutions.org/"><img title="Privacy Solutions" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Privacy-Solutions.svg" width="190" /></a>
|
||||
<a href="https://www.emsys-renewables.com/"><img title="emsys renewables" src="https://www.attrs.org/en/25.3.0/_static/sponsors/emsys-renewables.svg" width="190" /></a>
|
||||
<a href="https://filepreviews.io/"><img title="FilePreviews" src="https://www.attrs.org/en/25.3.0/_static/sponsors/FilePreviews.svg" width="190" /></a>
|
||||
<a href="https://polar.sh/"><img title="Polar" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Polar.svg" width="190" /></a>
|
||||
<a href="https://www.variomedia.de/"><img title="Variomedia AG" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Variomedia.svg" width="190" /></a>
|
||||
<a href="https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek"><img title="Tidelift" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Tidelift.svg" width="190" /></a>
|
||||
<a href="https://kraken.tech/"><img title="Kraken Tech" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Kraken.svg" width="190" /></a>
|
||||
<a href="https://privacy-solutions.org/"><img title="Privacy Solutions" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Privacy-Solutions.svg" width="190" /></a>
|
||||
<a href="https://filepreviews.io/"><img title="FilePreviews" src="https://www.attrs.org/en/26.1.0/_static/sponsors/FilePreviews.svg" width="190" /></a>
|
||||
<a href="https://www.testmuai.com/?utm_medium=sponsor&utm_source=structlog"><img title="TestMu AI" src="https://www.attrs.org/en/26.1.0/_static/sponsors/TestMu-AI.svg" width="190" /></a>
|
||||
<a href="https://polar.sh/"><img title="Polar" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Polar.svg" width="190" /></a>
|
||||
<!-- [[[end]]] -->
|
||||
|
||||
</p>
|
||||
@@ -220,10 +174,23 @@ Save time, reduce risk, and improve code health, while paying the maintainers of
|
||||
|
||||
## Release Information
|
||||
|
||||
### Backwards-incompatible Changes
|
||||
|
||||
- Field aliases are now resolved *before* calling `field_transformer`, so transformers receive fully populated `Attribute` objects with usable `alias` values instead of `None`.
|
||||
The new `Attribute.alias_is_default` flag indicates whether the alias was auto-generated (`True`) or explicitly set by the user (`False`).
|
||||
[#1509](https://github.com/python-attrs/attrs/issues/1509)
|
||||
|
||||
|
||||
### Changes
|
||||
|
||||
- Restore support for generator-based `field_transformer`s.
|
||||
[#1417](https://github.com/python-attrs/attrs/issues/1417)
|
||||
- Fix type annotations for `attrs.validators.optional()`, so it no longer rejects tuples with more than one validator.
|
||||
[#1496](https://github.com/python-attrs/attrs/issues/1496)
|
||||
- The `attrs.validators.disabled()` contextmanager can now be nested.
|
||||
[#1513](https://github.com/python-attrs/attrs/issues/1513)
|
||||
- Frozen classes can set `on_setattr=attrs.setters.NO_OP` in addition to `None`.
|
||||
[#1515](https://github.com/python-attrs/attrs/issues/1515)
|
||||
- It's now possible to pass *attrs* **instances** in addition to *attrs* **classes** to `attrs.fields()`.
|
||||
[#1529](https://github.com/python-attrs/attrs/issues/1529)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057
|
||||
attr/__init__.pyi,sha256=pVGImAUVovq2_TYl_r_HIYnGlyOaoCuEhxo-EvsnnSc,11325
|
||||
attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117
|
||||
attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368
|
||||
attr/_compat.py,sha256=x0g7iEUOnBVJC72zyFCgb1eKqyxS-7f2LGnNyZ_r95s,2829
|
||||
attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843
|
||||
attr/_funcs.py,sha256=Ix5IETTfz5F01F-12MF_CSFomIn2h8b67EVVz2gCtBE,16479
|
||||
attr/_make.py,sha256=H7OH2eWS5CnBzLUjNFE1WymfPrmF1r8fv2RPdt9MuYA,106129
|
||||
attr/_next_gen.py,sha256=BQtCUlzwg2gWHTYXBQvrEYBnzBUrDvO57u0Py6UCPhc,26274
|
||||
attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469
|
||||
attr/_version_info.py,sha256=w4R-FYC3NK_kMkGUWJlYP4cVAlH9HRaC-um3fcjYkHM,2222
|
||||
attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209
|
||||
attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861
|
||||
attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643
|
||||
attr/exceptions.py,sha256=b4vMbnoQ3VpwWZhqrYi_ssXVCK8o2c4HQSS09cSUM9o,1990
|
||||
attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539
|
||||
attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795
|
||||
attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208
|
||||
attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617
|
||||
attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584
|
||||
attr/validators.py,sha256=m3QRzZTANr4f2C4eVdUoFg11NgXWak8Wat4qQTGhvcs,21553
|
||||
attr/validators.pyi,sha256=gM1ZmHaBckyYWI2EirpRNzqm3B19cw5Iq6B4Kno9YCM,4087
|
||||
attrs-26.1.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
attrs-26.1.0.dist-info/METADATA,sha256=TNQOaQ8jvzfLytNO_WdY4GLfHfB8hoM_fjzpW_H6OMw,8754
|
||||
attrs-26.1.0.dist-info/RECORD,,
|
||||
attrs-26.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
attrs-26.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
||||
attrs-26.1.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109
|
||||
attrs/__init__.py,sha256=RxaAZNwYiEh-fcvHLZNpQ_DWKni73M_jxEPEftiq1Zc,1183
|
||||
attrs/__init__.pyi,sha256=2gV79g9UxJppGSM48hAZJ6h_MHb70dZoJL31ZNJeZYI,9416
|
||||
attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76
|
||||
attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76
|
||||
attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73
|
||||
attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73
|
||||
attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.27.0
|
||||
Generator: hatchling 1.29.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@@ -22,7 +22,8 @@ from attr import (
|
||||
resolve_types,
|
||||
validate,
|
||||
)
|
||||
from attr._next_gen import asdict, astuple
|
||||
from attr._make import ClassProps
|
||||
from attr._next_gen import asdict, astuple, inspect
|
||||
|
||||
from . import converters, exceptions, filters, setters, validators
|
||||
|
||||
@@ -31,6 +32,7 @@ __all__ = [
|
||||
"NOTHING",
|
||||
"Attribute",
|
||||
"AttrsInstance",
|
||||
"ClassProps",
|
||||
"Converter",
|
||||
"Factory",
|
||||
"NothingType",
|
||||
@@ -58,6 +60,7 @@ __all__ = [
|
||||
"filters",
|
||||
"frozen",
|
||||
"has",
|
||||
"inspect",
|
||||
"make_class",
|
||||
"mutable",
|
||||
"resolve_types",
|
||||
|
||||
@@ -77,7 +77,7 @@ def field(
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
converter: None = ...,
|
||||
factory: None = ...,
|
||||
kw_only: bool = ...,
|
||||
kw_only: bool | None = ...,
|
||||
eq: bool | None = ...,
|
||||
order: bool | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
@@ -98,10 +98,10 @@ def field(
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
converter: _ConverterType
|
||||
| list[_ConverterType]
|
||||
| tuple[_ConverterType]
|
||||
| tuple[_ConverterType, ...]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
kw_only: bool | None = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
@@ -121,10 +121,10 @@ def field(
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
converter: _ConverterType
|
||||
| list[_ConverterType]
|
||||
| tuple[_ConverterType]
|
||||
| tuple[_ConverterType, ...]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
kw_only: bool | None = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
@@ -144,10 +144,10 @@ def field(
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
converter: _ConverterType
|
||||
| list[_ConverterType]
|
||||
| tuple[_ConverterType]
|
||||
| tuple[_ConverterType, ...]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
kw_only: bool | None = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
@@ -261,3 +261,54 @@ def frozen(
|
||||
field_transformer: _FieldTransformer | None = ...,
|
||||
match_args: bool = ...,
|
||||
) -> Callable[[_C], _C]: ...
|
||||
|
||||
class ClassProps:
|
||||
# XXX: somehow when defining/using enums Mypy starts looking at our own
|
||||
# (untyped) code and causes tons of errors.
|
||||
Hashability: Any
|
||||
KeywordOnly: Any
|
||||
|
||||
is_exception: bool
|
||||
is_slotted: bool
|
||||
has_weakref_slot: bool
|
||||
is_frozen: bool
|
||||
# kw_only: ClassProps.KeywordOnly
|
||||
kw_only: Any
|
||||
collected_fields_by_mro: bool
|
||||
added_init: bool
|
||||
added_repr: bool
|
||||
added_eq: bool
|
||||
added_ordering: bool
|
||||
# hashability: ClassProps.Hashability
|
||||
hashability: Any
|
||||
added_match_args: bool
|
||||
added_str: bool
|
||||
added_pickling: bool
|
||||
on_setattr_hook: _OnSetAttrType | None
|
||||
field_transformer: Callable[[Attribute[Any]], Attribute[Any]] | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_exception: bool,
|
||||
is_slotted: bool,
|
||||
has_weakref_slot: bool,
|
||||
is_frozen: bool,
|
||||
# kw_only: ClassProps.KeywordOnly
|
||||
kw_only: Any,
|
||||
collected_fields_by_mro: bool,
|
||||
added_init: bool,
|
||||
added_repr: bool,
|
||||
added_eq: bool,
|
||||
added_ordering: bool,
|
||||
# hashability: ClassProps.Hashability
|
||||
hashability: Any,
|
||||
added_match_args: bool,
|
||||
added_str: bool,
|
||||
added_pickling: bool,
|
||||
on_setattr_hook: _OnSetAttrType,
|
||||
field_transformer: Callable[[Attribute[Any]], Attribute[Any]],
|
||||
) -> None: ...
|
||||
@property
|
||||
def is_hashable(self) -> bool: ...
|
||||
|
||||
def inspect(cls: type) -> ClassProps: ...
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
pip
|
||||
@@ -1,102 +0,0 @@
|
||||
cattr/__init__.py,sha256=bYrmwTYSdYC_ut1xW31V7mxhXBlJQKs8EECgtUBgAuc,906
|
||||
cattr/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattr/__pycache__/converters.cpython-311.pyc,,
|
||||
cattr/__pycache__/disambiguators.cpython-311.pyc,,
|
||||
cattr/__pycache__/dispatch.cpython-311.pyc,,
|
||||
cattr/__pycache__/errors.cpython-311.pyc,,
|
||||
cattr/__pycache__/gen.cpython-311.pyc,,
|
||||
cattr/converters.py,sha256=rQhY4J8r7QTZh5WICuFe4GWO1v0DS3DgQ9r569zd6jg,192
|
||||
cattr/disambiguators.py,sha256=ugD1fq1Z5x1pGu5P1lMzcT-IEi1q7IfQJIHEdmg62vM,103
|
||||
cattr/dispatch.py,sha256=uVEOgHWR9Hn5tm-wIw-bDccqrxJByVi8yRKaYyvL67k,125
|
||||
cattr/errors.py,sha256=V4RhoCObwGrlaM3oyn1H_FYxGR8iAB9dG5NxFDYM548,343
|
||||
cattr/gen.py,sha256=hWyKoZ_d2D36Jz_npspyGw8s9pWtUA69sXf0R3uOvgM,597
|
||||
cattr/preconf/__init__.py,sha256=NqPE7uhVfcP-PggkUpsbfAutMo8oHjcoB1cvjgLft-s,78
|
||||
cattr/preconf/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/bson.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/json.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/msgpack.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/orjson.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/pyyaml.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/tomlkit.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/ujson.cpython-311.pyc,,
|
||||
cattr/preconf/bson.py,sha256=Bn4hJxac7OthGg_CR4LCPeBp_fz4kx3QniBVOZhguGs,195
|
||||
cattr/preconf/json.py,sha256=LpqYuO3oePDxbQtKFKB0SaoeAi3Z_agIgyNn1VQSIVo,206
|
||||
cattr/preconf/msgpack.py,sha256=pyJ9L9ekNlZ0IQHbJ9Ay_fi_NOqY5_rE_q-UnD94-RM,207
|
||||
cattr/preconf/orjson.py,sha256=Adh-7csx4eqCjx22zipMFgSlDXbR554wvgNHEb8Q5JM,203
|
||||
cattr/preconf/pyyaml.py,sha256=Fy40bejjp7uqgoLhTA_p4wZYF0uFaguHbUK9zs9LoC0,203
|
||||
cattr/preconf/tomlkit.py,sha256=_gADJ_UYpj3EiNXGYjAfSOkcoFIkLpYVOFfLEqBfIJQ,207
|
||||
cattr/preconf/ujson.py,sha256=IzEa7QUcYOaSUMiLQsFEWJnBihmmOLhehsM-5cPY9NI,199
|
||||
cattr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cattrs-25.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
cattrs-25.1.1.dist-info/METADATA,sha256=ODqSak3dhIZZjmFa-SZT8Si32_3ey_oo2tUefYx0QtU,8388
|
||||
cattrs-25.1.1.dist-info/RECORD,,
|
||||
cattrs-25.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cattrs-25.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
||||
cattrs-25.1.1.dist-info/licenses/LICENSE,sha256=9fudHt43qIykf0IMSZ3KD0oFvJk-Esd9I1IKrSkcAb8,1074
|
||||
cattrs/__init__.py,sha256=UhiFdxf81gCuBBA6FutoE1oOzthzF_PkAdoE2AVslIo,1901
|
||||
cattrs/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattrs/__pycache__/_compat.cpython-311.pyc,,
|
||||
cattrs/__pycache__/_generics.cpython-311.pyc,,
|
||||
cattrs/__pycache__/cols.cpython-311.pyc,,
|
||||
cattrs/__pycache__/converters.cpython-311.pyc,,
|
||||
cattrs/__pycache__/disambiguators.cpython-311.pyc,,
|
||||
cattrs/__pycache__/dispatch.cpython-311.pyc,,
|
||||
cattrs/__pycache__/errors.cpython-311.pyc,,
|
||||
cattrs/__pycache__/fns.cpython-311.pyc,,
|
||||
cattrs/__pycache__/literals.cpython-311.pyc,,
|
||||
cattrs/__pycache__/typealiases.cpython-311.pyc,,
|
||||
cattrs/__pycache__/types.cpython-311.pyc,,
|
||||
cattrs/__pycache__/v.cpython-311.pyc,,
|
||||
cattrs/_compat.py,sha256=dMRB8a8RkxFdnQDKRpamresVF_SBkksM2_ZMAuL0s2w,11987
|
||||
cattrs/_generics.py,sha256=keExDE2CGIer8ci12SoJ_rXYTLva9P29uLlvyb_fxtM,966
|
||||
cattrs/cols.py,sha256=mWDchfvjMQ6uACKSfdZs05YiXrWdph2HOJfaqY3D_EI,8848
|
||||
cattrs/converters.py,sha256=ui4BSAxnV1J6Oh0pYN2oOHgp-mIybsXW-M8SXPDNzlo,54262
|
||||
cattrs/disambiguators.py,sha256=eUyWMtW6bQJcXGOWiraq1pFcMlUbo9BJHxFY--KF6Lk,6867
|
||||
cattrs/dispatch.py,sha256=9qA-pmsPvgrM6MGP8Ev2gVP6YL2rXvoBla_C0VgHxQ0,6780
|
||||
cattrs/errors.py,sha256=6IGfE-wVQbOaDNN4xAQJf7Hk_t2QNV0Zem64D6yMZrU,4168
|
||||
cattrs/fns.py,sha256=z5z1VZOZv8t5LwG8cBM_tIXg-_PlQUOyZb9wIrXNqlw,626
|
||||
cattrs/gen/__init__.py,sha256=bpRGHd3G0UTpWcHRcJTNAUcWmX-evoPHGvpH6u29FDU,38842
|
||||
cattrs/gen/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/_consts.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/_generics.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/_lc.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/_shared.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/typeddicts.cpython-311.pyc,,
|
||||
cattrs/gen/_consts.py,sha256=ZwT_m2J3S7p-UjltpbA1WtfQZLNj9KhmFYCAv6Zl-g0,511
|
||||
cattrs/gen/_generics.py,sha256=_DyXCGql2QIxGhAv3_B1hsi80uPK8PhK2hhZa95YOlo,3011
|
||||
cattrs/gen/_lc.py,sha256=4fjeUsmgQcCAIjnNndBic0gf5qKmxVS3CZHqUQ9Rw5g,882
|
||||
cattrs/gen/_shared.py,sha256=xKsfcVtpyYIir9AW8VuOVoiSbaEI7tsSL0JpUCIUX-g,2296
|
||||
cattrs/gen/typeddicts.py,sha256=Ck3QMr_B1T7vwxyRjfZPHafKphN2hndL181dpQNxzPs,21254
|
||||
cattrs/literals.py,sha256=0kzAewmWk9ikJGoKq4ysnAR22DMawG3iNqLl8NLgpk0,331
|
||||
cattrs/preconf/__init__.py,sha256=P7czFRcjeN6zBcdwUyeBloniltlJptCa8Yd2uFGlz9w,1527
|
||||
cattrs/preconf/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/bson.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/cbor2.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/json.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/msgpack.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/msgspec.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/orjson.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/pyyaml.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/tomlkit.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/ujson.cpython-311.pyc,,
|
||||
cattrs/preconf/bson.py,sha256=6p1kmOFMjswSXFCb1hKJeNvr3kNsAm1gfX_DA6igq8E,4201
|
||||
cattrs/preconf/cbor2.py,sha256=LnREcjpOp_402poUGRVIhDWI4f_R1wvJkdKvs5MrTGU,2022
|
||||
cattrs/preconf/json.py,sha256=zTrkfjOxXZFwwabNESeafy-C7MEpn7Cw5AdhkkeOjU4,2631
|
||||
cattrs/preconf/msgpack.py,sha256=dZE9tsAA5qX3pSc3MZmlGuvJ5q_wI6mANDyugKXKj-E,2325
|
||||
cattrs/preconf/msgspec.py,sha256=Ds0rPW4900zsBLqfupF-smkg9_Kwyx7D_Vh9a0yJB8M,7250
|
||||
cattrs/preconf/orjson.py,sha256=5MBcUsyp3eGsHgLfLtt8-q90L2mxjD0ttnrWBUIwouo,3870
|
||||
cattrs/preconf/pyyaml.py,sha256=w0aM_gJ6VhZf-Zpu_UlJki7rdgv4mfaSXElPofB3nlE,2378
|
||||
cattrs/preconf/tomlkit.py,sha256=gJWGJjMONCViTMZuphOg2xXzjQt3SCEVFVdoKgDjqc8,3148
|
||||
cattrs/preconf/ujson.py,sha256=wRLidBM8aWucFkCQ9haiktY8xYoCdanDhQuKJLQJgGM,2425
|
||||
cattrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cattrs/strategies/__init__.py,sha256=nkZWCzSRYcS-75FMfk52mioZSuWykaN8hB39Vig5Xkg,339
|
||||
cattrs/strategies/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattrs/strategies/__pycache__/_class_methods.cpython-311.pyc,,
|
||||
cattrs/strategies/__pycache__/_subclasses.cpython-311.pyc,,
|
||||
cattrs/strategies/__pycache__/_unions.cpython-311.pyc,,
|
||||
cattrs/strategies/_class_methods.py,sha256=O5xhQCzNpuFiDNDMlbcyeOVqyrV65NhMZNRsG3jnoBU,2591
|
||||
cattrs/strategies/_subclasses.py,sha256=aCE2UQjevZQHMnPOPyl2qR_hgRpgRUt1j9lE4qZ3hNc,9365
|
||||
cattrs/strategies/_unions.py,sha256=YBBklVSWJ-7DSkLDLpumwAJJ39ALuSGyB6W0Ptz5Rz4,9355
|
||||
cattrs/typealiases.py,sha256=toHavC2kJsIcxThwvATPO5JShzKeC8kIl9KqteFohbw,1619
|
||||
cattrs/types.py,sha256=cqvfmzliYfrvPswxlW_tN4DmhQ2xpAKQvVbNBJaxiWs,278
|
||||
cattrs/v.py,sha256=IqUajgJFCKJYf-4S9TCKRtJcmmK4c3En69TGuf2FKOs,4126
|
||||
@@ -0,0 +1 @@
|
||||
uv
|
||||
+9
-6
@@ -1,6 +1,6 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: cattrs
|
||||
Version: 25.1.1
|
||||
Version: 26.1.0
|
||||
Summary: Composable complex class support for attrs and dataclasses.
|
||||
Project-URL: Homepage, https://catt.rs
|
||||
Project-URL: Changelog, https://catt.rs/en/latest/history.html
|
||||
@@ -14,18 +14,18 @@ Keywords: attrs,dataclasses,serialization
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
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: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.9
|
||||
Requires-Dist: attrs>=24.3.0
|
||||
Requires-Python: >=3.10
|
||||
Requires-Dist: attrs>=25.4.0
|
||||
Requires-Dist: exceptiongroup>=1.1.1; python_version < '3.11'
|
||||
Requires-Dist: typing-extensions>=4.12.2
|
||||
Requires-Dist: typing-extensions>=4.14.0
|
||||
Provides-Extra: bson
|
||||
Requires-Dist: pymongo>=4.4.0; extra == 'bson'
|
||||
Provides-Extra: cbor2
|
||||
@@ -35,11 +35,14 @@ Requires-Dist: msgpack>=1.0.5; extra == 'msgpack'
|
||||
Provides-Extra: msgspec
|
||||
Requires-Dist: msgspec>=0.19.0; (implementation_name == 'cpython') and extra == 'msgspec'
|
||||
Provides-Extra: orjson
|
||||
Requires-Dist: orjson>=3.10.7; (implementation_name == 'cpython') and extra == 'orjson'
|
||||
Requires-Dist: orjson>=3.11.3; (implementation_name == 'cpython') and extra == 'orjson'
|
||||
Provides-Extra: pyyaml
|
||||
Requires-Dist: pyyaml>=6.0; extra == 'pyyaml'
|
||||
Provides-Extra: tomlkit
|
||||
Requires-Dist: tomlkit>=0.11.8; extra == 'tomlkit'
|
||||
Provides-Extra: tomllib
|
||||
Requires-Dist: tomli-w>=1.1.0; extra == 'tomllib'
|
||||
Requires-Dist: tomli>=1.1.0; (python_version < '3.11') and extra == 'tomllib'
|
||||
Provides-Extra: ujson
|
||||
Requires-Dist: ujson>=5.10.0; extra == 'ujson'
|
||||
Description-Content-Type: text/markdown
|
||||
@@ -0,0 +1,58 @@
|
||||
cattr/__init__.py,sha256=bYrmwTYSdYC_ut1xW31V7mxhXBlJQKs8EECgtUBgAuc,906
|
||||
cattr/converters.py,sha256=rQhY4J8r7QTZh5WICuFe4GWO1v0DS3DgQ9r569zd6jg,192
|
||||
cattr/disambiguators.py,sha256=ugD1fq1Z5x1pGu5P1lMzcT-IEi1q7IfQJIHEdmg62vM,103
|
||||
cattr/dispatch.py,sha256=uVEOgHWR9Hn5tm-wIw-bDccqrxJByVi8yRKaYyvL67k,125
|
||||
cattr/errors.py,sha256=V4RhoCObwGrlaM3oyn1H_FYxGR8iAB9dG5NxFDYM548,343
|
||||
cattr/gen.py,sha256=hWyKoZ_d2D36Jz_npspyGw8s9pWtUA69sXf0R3uOvgM,597
|
||||
cattr/preconf/__init__.py,sha256=NqPE7uhVfcP-PggkUpsbfAutMo8oHjcoB1cvjgLft-s,78
|
||||
cattr/preconf/bson.py,sha256=Bn4hJxac7OthGg_CR4LCPeBp_fz4kx3QniBVOZhguGs,195
|
||||
cattr/preconf/json.py,sha256=LpqYuO3oePDxbQtKFKB0SaoeAi3Z_agIgyNn1VQSIVo,206
|
||||
cattr/preconf/msgpack.py,sha256=pyJ9L9ekNlZ0IQHbJ9Ay_fi_NOqY5_rE_q-UnD94-RM,207
|
||||
cattr/preconf/orjson.py,sha256=Adh-7csx4eqCjx22zipMFgSlDXbR554wvgNHEb8Q5JM,203
|
||||
cattr/preconf/pyyaml.py,sha256=Fy40bejjp7uqgoLhTA_p4wZYF0uFaguHbUK9zs9LoC0,203
|
||||
cattr/preconf/tomlkit.py,sha256=_gADJ_UYpj3EiNXGYjAfSOkcoFIkLpYVOFfLEqBfIJQ,207
|
||||
cattr/preconf/ujson.py,sha256=IzEa7QUcYOaSUMiLQsFEWJnBihmmOLhehsM-5cPY9NI,199
|
||||
cattr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cattrs-26.1.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
cattrs-26.1.0.dist-info/METADATA,sha256=d94QudQ0gM0Zy_KNPtL7n2gEea7s-Vir2t4_rLD4zLA,8542
|
||||
cattrs-26.1.0.dist-info/RECORD,,
|
||||
cattrs-26.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cattrs-26.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
||||
cattrs-26.1.0.dist-info/licenses/LICENSE,sha256=9fudHt43qIykf0IMSZ3KD0oFvJk-Esd9I1IKrSkcAb8,1074
|
||||
cattrs/__init__.py,sha256=UhiFdxf81gCuBBA6FutoE1oOzthzF_PkAdoE2AVslIo,1901
|
||||
cattrs/_compat.py,sha256=5dOpD6O8zUVZcreaJ5wSt9n-Nut4n79ibrka5Td6l84,12251
|
||||
cattrs/_generics.py,sha256=ERYo_kX-Z6UiwOcYI3WLrlHO6Ya66c_whpko6es5UeI,966
|
||||
cattrs/cols.py,sha256=-KmFikvZu7cL_g68ytx0Ad6danfjVY5Ry3w1XRtTaWk,10336
|
||||
cattrs/converters.py,sha256=BPpSPuhMuhQz2qFl_OAeIZ8CrI95A8gy6khuvU35bN8,55322
|
||||
cattrs/disambiguators.py,sha256=uydD6QXPve1-0dY_3m_FoyOqowvTvEKXzrdb-5xkRPE,6863
|
||||
cattrs/dispatch.py,sha256=9qA-pmsPvgrM6MGP8Ev2gVP6YL2rXvoBla_C0VgHxQ0,6780
|
||||
cattrs/enums.py,sha256=noADdwXP9TSpyIyjE6WR2iIbGPmigirvArKAoHWJmDI,1130
|
||||
cattrs/errors.py,sha256=CLW6Uev31cOxo6cWFb0-g-FAbYJD2mYmR5xTxDIVjDw,4344
|
||||
cattrs/fns.py,sha256=z5z1VZOZv8t5LwG8cBM_tIXg-_PlQUOyZb9wIrXNqlw,626
|
||||
cattrs/gen/__init__.py,sha256=wrmi87jGEfmWcbbNkZXk1Gqn9PLwXerKUDJoCQBCRek,42553
|
||||
cattrs/gen/_consts.py,sha256=ZwT_m2J3S7p-UjltpbA1WtfQZLNj9KhmFYCAv6Zl-g0,511
|
||||
cattrs/gen/_generics.py,sha256=_DyXCGql2QIxGhAv3_B1hsi80uPK8PhK2hhZa95YOlo,3011
|
||||
cattrs/gen/_lc.py,sha256=4fjeUsmgQcCAIjnNndBic0gf5qKmxVS3CZHqUQ9Rw5g,882
|
||||
cattrs/gen/_shared.py,sha256=QctJIsgfhZqgdALdD-L9FUEX1k2uNUpJQW4NbcGLVWQ,2747
|
||||
cattrs/gen/typeddicts.py,sha256=svG8RjqNt411KzkRFEKp6zsJHJvrwZNe1UQIhQewv7w,22085
|
||||
cattrs/literals.py,sha256=0kzAewmWk9ikJGoKq4ysnAR22DMawG3iNqLl8NLgpk0,331
|
||||
cattrs/preconf/__init__.py,sha256=YMY1ADc7OzpuZ2cdQZbH5kUKZF30UYd6owf3BOT4TO0,1435
|
||||
cattrs/preconf/bson.py,sha256=I0632WE3L7VN5aA9T3PJ1Jq_iy5XOfTsjJcKjQSEMAM,4211
|
||||
cattrs/preconf/cbor2.py,sha256=o13uyALyX2dvxp6HRYq_xHeB84nEGyBOB0V8OcohY30,2035
|
||||
cattrs/preconf/json.py,sha256=JiAetUezqQ_ixjRydLCavkvnIQxQCTyQXKzViC-vO6A,2644
|
||||
cattrs/preconf/msgpack.py,sha256=12c3sE5tfIVVhxHD4QmZaSGmxTvwHfrtTdk0ohOCcLI,2338
|
||||
cattrs/preconf/msgspec.py,sha256=Hea-jCkcqH7wtONAHjpe9EaaqhGodQFfkLtKuWdV09k,7384
|
||||
cattrs/preconf/orjson.py,sha256=o1GZwvH2hlLbovmsB7OH3fDkcROhHOAQKMsrDW3VTsY,3883
|
||||
cattrs/preconf/pyyaml.py,sha256=w0aM_gJ6VhZf-Zpu_UlJki7rdgv4mfaSXElPofB3nlE,2378
|
||||
cattrs/preconf/tomlkit.py,sha256=sPrBZgq7Zg78y31SAPhBuSWQllGxXXysyI-2-T6MY6U,3311
|
||||
cattrs/preconf/tomllib.py,sha256=wDOhVcIkBR0bw5zMACPlUcf2YXv-eamgg-GmGdv4cSU,3083
|
||||
cattrs/preconf/ujson.py,sha256=ev1fdMvQdP-PSDqDbJ714Oy3Xxi2lbc6qVSZ-LGdgKg,2438
|
||||
cattrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cattrs/strategies/__init__.py,sha256=nkZWCzSRYcS-75FMfk52mioZSuWykaN8hB39Vig5Xkg,339
|
||||
cattrs/strategies/_class_methods.py,sha256=O5xhQCzNpuFiDNDMlbcyeOVqyrV65NhMZNRsG3jnoBU,2591
|
||||
cattrs/strategies/_subclasses.py,sha256=8fTr7CLbNlV2Nobi0yXkQ9OKpGyUENmEzyYAsy4SDJw,9852
|
||||
cattrs/strategies/_unions.py,sha256=XQwrnPEE7KhBqOy5vyUZ06reQPfqcO85jdVjQ7i3HFk,10070
|
||||
cattrs/subclasses.py,sha256=SyAaJ84sO3aen_dBQkuSAIg0eGOXYX-aF151d7IbiNk,783
|
||||
cattrs/typealiases.py,sha256=toHavC2kJsIcxThwvATPO5JShzKeC8kIl9KqteFohbw,1619
|
||||
cattrs/types.py,sha256=cqvfmzliYfrvPswxlW_tN4DmhQ2xpAKQvVbNBJaxiWs,278
|
||||
cattrs/v.py,sha256=IqUajgJFCKJYf-4S9TCKRtJcmmK4c3En69TGuf2FKOs,4126
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.27.0
|
||||
Generator: hatchling 1.28.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@@ -29,7 +29,6 @@ from typing import (
|
||||
_AnnotatedAlias,
|
||||
_GenericAlias,
|
||||
_SpecialGenericAlias,
|
||||
_UnionGenericAlias,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
@@ -209,10 +208,7 @@ def get_final_base(type) -> Optional[type]:
|
||||
OriginAbstractSet = AbcSet
|
||||
OriginMutableSet = AbcMutableSet
|
||||
|
||||
signature = _signature
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
signature = partial(_signature, eval_str=True)
|
||||
signature = partial(_signature, eval_str=True)
|
||||
|
||||
|
||||
try:
|
||||
@@ -256,10 +252,25 @@ def is_tuple(type):
|
||||
)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
if sys.version_info >= (3, 14):
|
||||
|
||||
def is_union_type(obj):
|
||||
from types import UnionType
|
||||
from types import UnionType # noqa: PLC0415
|
||||
|
||||
return obj is Union or isinstance(obj, UnionType)
|
||||
|
||||
def get_newtype_base(typ: Any) -> Optional[type]:
|
||||
if typ is NewType or isinstance(typ, NewType):
|
||||
return typ.__supertype__
|
||||
return None
|
||||
|
||||
from typing import NotRequired, Required
|
||||
|
||||
else:
|
||||
from typing import _UnionGenericAlias
|
||||
|
||||
def is_union_type(obj):
|
||||
from types import UnionType # noqa: PLC0415
|
||||
|
||||
return (
|
||||
obj is Union
|
||||
@@ -277,25 +288,6 @@ if sys.version_info >= (3, 10):
|
||||
else:
|
||||
from typing_extensions import NotRequired, Required
|
||||
|
||||
else:
|
||||
# 3.9
|
||||
from typing_extensions import NotRequired, Required
|
||||
|
||||
def is_union_type(obj):
|
||||
return obj is Union or (
|
||||
isinstance(obj, _UnionGenericAlias) and obj.__origin__ is Union
|
||||
)
|
||||
|
||||
def get_newtype_base(typ: Any) -> Optional[type]:
|
||||
supertype = getattr(typ, "__supertype__", None)
|
||||
if (
|
||||
supertype is not None
|
||||
and getattr(typ, "__qualname__", "") == "NewType.<locals>.new_type"
|
||||
and typ.__module__ in ("typing", "typing_extensions")
|
||||
):
|
||||
return supertype
|
||||
return None
|
||||
|
||||
|
||||
def get_notrequired_base(type) -> Union[Any, NothingType]:
|
||||
if is_annotated(type):
|
||||
@@ -306,6 +298,25 @@ def get_notrequired_base(type) -> Union[Any, NothingType]:
|
||||
return NOTHING
|
||||
|
||||
|
||||
def is_mutable_sequence(type: Any) -> bool:
|
||||
"""A predicate function for mutable sequences.
|
||||
|
||||
Matches lists, mutable sequences, and deques.
|
||||
"""
|
||||
origin = getattr(type, "__origin__", None)
|
||||
return (
|
||||
type in (List, list, TypingMutableSequence, AbcMutableSequence, deque, Deque)
|
||||
or (
|
||||
type.__class__ is _GenericAlias
|
||||
and (
|
||||
((origin is not tuple) and is_subclass(origin, TypingMutableSequence))
|
||||
or (origin is tuple and type.__args__[1] is ...)
|
||||
)
|
||||
)
|
||||
or (origin in (list, deque, AbcMutableSequence))
|
||||
)
|
||||
|
||||
|
||||
def is_sequence(type: Any) -> bool:
|
||||
"""A predicate function for sequences.
|
||||
|
||||
@@ -313,19 +324,8 @@ def is_sequence(type: Any) -> bool:
|
||||
tuples.
|
||||
"""
|
||||
origin = getattr(type, "__origin__", None)
|
||||
return (
|
||||
type
|
||||
in (
|
||||
List,
|
||||
list,
|
||||
TypingSequence,
|
||||
TypingMutableSequence,
|
||||
AbcMutableSequence,
|
||||
tuple,
|
||||
Tuple,
|
||||
deque,
|
||||
Deque,
|
||||
)
|
||||
return is_mutable_sequence(type) or (
|
||||
type in (TypingSequence, tuple, Tuple)
|
||||
or (
|
||||
type.__class__ is _GenericAlias
|
||||
and (
|
||||
@@ -333,7 +333,7 @@ def is_sequence(type: Any) -> bool:
|
||||
or (origin is tuple and type.__args__[1] is ...)
|
||||
)
|
||||
)
|
||||
or (origin in (list, deque, AbcMutableSequence, AbcSequence))
|
||||
or (origin is AbcSequence)
|
||||
or (origin is tuple and type.__args__[1] is ...)
|
||||
)
|
||||
|
||||
@@ -403,8 +403,10 @@ def is_generic(type) -> bool:
|
||||
"""Whether `type` is a generic type."""
|
||||
# Inheriting from protocol will inject `Generic` into the MRO
|
||||
# without `__orig_bases__`.
|
||||
return isinstance(type, (_GenericAlias, GenericAlias)) or (
|
||||
is_subclass(type, Generic) and hasattr(type, "__orig_bases__")
|
||||
return (
|
||||
isinstance(type, (_GenericAlias, GenericAlias))
|
||||
or (is_subclass(type, Generic) and hasattr(type, "__orig_bases__"))
|
||||
or type.__class__ is Union # On 3.14, unions are no longer typing._GenericAlias
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
from typing import Any, get_args
|
||||
|
||||
from attrs import NOTHING
|
||||
from typing_extensions import Self
|
||||
|
||||
from ._compat import copy_with, get_args, is_annotated, is_generic
|
||||
from ._compat import copy_with, is_annotated, is_generic
|
||||
|
||||
|
||||
def deep_copy_with(t, mapping: Mapping[str, Any], self_is=NOTHING):
|
||||
|
||||
+54
-10
@@ -5,25 +5,20 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterable
|
||||
from functools import partial
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
DefaultDict,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
TypeVar,
|
||||
get_type_hints,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Any, DefaultDict, Literal, NamedTuple, TypeVar
|
||||
|
||||
from attrs import NOTHING, Attribute, NothingType
|
||||
|
||||
from ._compat import (
|
||||
ANIES,
|
||||
AbcSet,
|
||||
get_args,
|
||||
get_full_type_hints,
|
||||
get_origin,
|
||||
is_bare,
|
||||
is_frozenset,
|
||||
is_mapping,
|
||||
is_mutable_sequence,
|
||||
is_sequence,
|
||||
is_subclass,
|
||||
)
|
||||
@@ -47,10 +42,13 @@ if TYPE_CHECKING:
|
||||
|
||||
__all__ = [
|
||||
"defaultdict_structure_factory",
|
||||
"homogenous_tuple_structure_factory",
|
||||
"is_abstract_set",
|
||||
"is_any_set",
|
||||
"is_defaultdict",
|
||||
"is_frozenset",
|
||||
"is_mapping",
|
||||
"is_mutable_sequence",
|
||||
"is_namedtuple",
|
||||
"is_sequence",
|
||||
"is_set",
|
||||
@@ -70,6 +68,11 @@ def is_any_set(type) -> bool:
|
||||
return is_set(type) or is_frozenset(type)
|
||||
|
||||
|
||||
def is_abstract_set(type) -> bool:
|
||||
"""A predicate function for abstract (collection.abc) sets."""
|
||||
return type is AbcSet or (getattr(type, "__origin__", None) is AbcSet)
|
||||
|
||||
|
||||
def is_namedtuple(type: Any) -> bool:
|
||||
"""A predicate function for named tuples."""
|
||||
|
||||
@@ -151,6 +154,47 @@ def list_structure_factory(type: type, converter: BaseConverter) -> StructureHoo
|
||||
return structure_list
|
||||
|
||||
|
||||
def homogenous_tuple_structure_factory(
|
||||
type: type, converter: BaseConverter
|
||||
) -> StructureHook:
|
||||
"""A hook factory for homogenous (all elements the same, indeterminate length) tuples.
|
||||
|
||||
Converts any given iterable into a tuple.
|
||||
"""
|
||||
|
||||
if is_bare(type) or type.__args__[0] in ANIES:
|
||||
|
||||
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
|
||||
return tuple(obj)
|
||||
|
||||
return structure_tuple
|
||||
|
||||
elem_type = type.__args__[0]
|
||||
|
||||
try:
|
||||
handler = converter.get_structure_hook(elem_type)
|
||||
except RecursionError:
|
||||
# Break the cycle by using late binding.
|
||||
handler = converter.structure
|
||||
|
||||
if converter.detailed_validation:
|
||||
|
||||
# We have to structure into a list first anyway.
|
||||
list_structure = list_structure_factory(type, converter)
|
||||
|
||||
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
|
||||
return tuple(list_structure(obj, _))
|
||||
|
||||
else:
|
||||
|
||||
def structure_tuple(
|
||||
obj: Iterable[T], _: type = type, _handler=handler, _elem_type=elem_type
|
||||
) -> tuple[T, ...]:
|
||||
return tuple([_handler(e, _elem_type) for e in obj])
|
||||
|
||||
return structure_tuple
|
||||
|
||||
|
||||
def namedtuple_unstructure_factory(
|
||||
cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None
|
||||
) -> UnstructureHook:
|
||||
@@ -195,7 +239,7 @@ def _namedtuple_to_attrs(cl: type[tuple]) -> list[Attribute]:
|
||||
type=a,
|
||||
alias=name,
|
||||
)
|
||||
for name, a in get_type_hints(cl).items()
|
||||
for name, a in get_full_type_hints(cl).items()
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -43,10 +43,11 @@ from ._compat import (
|
||||
is_hetero_tuple,
|
||||
is_literal,
|
||||
is_mapping,
|
||||
is_mutable_sequence,
|
||||
is_mutable_set,
|
||||
is_optional,
|
||||
is_protocol,
|
||||
is_sequence,
|
||||
is_subclass,
|
||||
is_tuple,
|
||||
is_typeddict,
|
||||
is_union_type,
|
||||
@@ -54,8 +55,11 @@ from ._compat import (
|
||||
)
|
||||
from .cols import (
|
||||
defaultdict_structure_factory,
|
||||
homogenous_tuple_structure_factory,
|
||||
is_abstract_set,
|
||||
is_defaultdict,
|
||||
is_namedtuple,
|
||||
is_sequence,
|
||||
iterable_unstructure_factory,
|
||||
list_structure_factory,
|
||||
mapping_structure_factory,
|
||||
@@ -73,6 +77,7 @@ from .dispatch import (
|
||||
UnstructuredValue,
|
||||
UnstructureHook,
|
||||
)
|
||||
from .enums import enum_structure_factory, enum_unstructure_factory
|
||||
from .errors import (
|
||||
IterableValidationError,
|
||||
IterableValidationNote,
|
||||
@@ -225,6 +230,10 @@ class BaseConverter:
|
||||
)
|
||||
self._unstructure_func.register_func_list(
|
||||
[
|
||||
(
|
||||
lambda t: get_newtype_base(t) is not None,
|
||||
lambda o: self.unstructure(o, unstructure_as=o.__class__),
|
||||
),
|
||||
(
|
||||
is_protocol,
|
||||
lambda o: self.unstructure(o, unstructure_as=o.__class__),
|
||||
@@ -239,12 +248,12 @@ class BaseConverter:
|
||||
lambda t: self.get_unstructure_hook(get_type_alias_base(t)),
|
||||
True,
|
||||
),
|
||||
(is_literal_containing_enums, self.unstructure),
|
||||
(is_mapping, self._unstructure_mapping),
|
||||
(is_sequence, self._unstructure_seq),
|
||||
(is_mutable_set, self._unstructure_seq),
|
||||
(is_frozenset, self._unstructure_seq),
|
||||
(lambda t: issubclass(t, Enum), self._unstructure_enum),
|
||||
(is_literal_containing_enums, self.unstructure),
|
||||
(lambda t: is_subclass(t, Enum), enum_unstructure_factory, "extended"),
|
||||
(has, self._unstructure_attrs),
|
||||
(is_union_type, self._unstructure_union),
|
||||
(lambda t: t in ANIES, self.unstructure),
|
||||
@@ -271,20 +280,27 @@ class BaseConverter:
|
||||
),
|
||||
(is_literal, self._structure_simple_literal),
|
||||
(is_literal_containing_enums, self._structure_enum_literal),
|
||||
(is_sequence, list_structure_factory, "extended"),
|
||||
(is_sequence, homogenous_tuple_structure_factory, "extended"),
|
||||
(is_mutable_sequence, list_structure_factory, "extended"),
|
||||
(is_deque, self._structure_deque),
|
||||
(is_mutable_set, self._structure_set),
|
||||
(is_abstract_set, self._structure_frozenset),
|
||||
(is_frozenset, self._structure_frozenset),
|
||||
(is_tuple, self._structure_tuple),
|
||||
(is_namedtuple, namedtuple_structure_factory, "extended"),
|
||||
(is_mapping, self._structure_dict),
|
||||
(is_supported_union, self._gen_attrs_union_structure, True),
|
||||
*(
|
||||
[(is_supported_union, self._gen_attrs_union_structure, True)]
|
||||
if unstruct_strat is UnstructureStrategy.AS_DICT
|
||||
else []
|
||||
),
|
||||
(is_optional, self._structure_optional),
|
||||
(
|
||||
lambda t: is_union_type(t) and t in self._union_struct_registry,
|
||||
self._union_struct_registry.__getitem__,
|
||||
True,
|
||||
),
|
||||
(lambda t: is_subclass(t, Enum), enum_structure_factory, "extended"),
|
||||
(has, self._structure_attrs),
|
||||
]
|
||||
)
|
||||
@@ -295,7 +311,6 @@ class BaseConverter:
|
||||
(bytes, self._structure_call),
|
||||
(int, self._structure_call),
|
||||
(float, self._structure_call),
|
||||
(Enum, self._structure_call),
|
||||
(Path, self._structure_call),
|
||||
]
|
||||
)
|
||||
@@ -617,10 +632,6 @@ class BaseConverter:
|
||||
res.append(dispatch(a.type or v.__class__)(v))
|
||||
return tuple(res)
|
||||
|
||||
def _unstructure_enum(self, obj: Enum) -> Any:
|
||||
"""Convert an enum to its value."""
|
||||
return obj.value
|
||||
|
||||
def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
|
||||
"""Convert a sequence to primitive equivalents."""
|
||||
# We can reuse the sequence class, so tuples stay tuples.
|
||||
@@ -1034,6 +1045,7 @@ class Converter(BaseConverter):
|
||||
"forbid_extra_keys",
|
||||
"omit_if_default",
|
||||
"type_overrides",
|
||||
"use_alias",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -1050,6 +1062,7 @@ class Converter(BaseConverter):
|
||||
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
|
||||
None, t
|
||||
),
|
||||
use_alias: bool = False,
|
||||
):
|
||||
"""
|
||||
:param detailed_validation: Whether to use a slightly slower mode for detailed
|
||||
@@ -1058,12 +1071,15 @@ class Converter(BaseConverter):
|
||||
registered unstructuring hooks match.
|
||||
:param structure_fallback_factory: A hook factory to be called when no
|
||||
registered structuring hooks match.
|
||||
:param use_alias: Whether to use the field alias instead of the field name as
|
||||
the un/structured dictionary key by default.
|
||||
|
||||
.. versionadded:: 23.2.0 *unstructure_fallback_factory*
|
||||
.. versionadded:: 23.2.0 *structure_fallback_factory*
|
||||
.. versionchanged:: 24.2.0
|
||||
The default `structure_fallback_factory` now raises errors for missing handlers
|
||||
more eagerly, surfacing problems earlier.
|
||||
.. versionadded:: 25.2.0 *use_alias*
|
||||
"""
|
||||
super().__init__(
|
||||
dict_factory=dict_factory,
|
||||
@@ -1076,6 +1092,7 @@ class Converter(BaseConverter):
|
||||
self.omit_if_default = omit_if_default
|
||||
self.forbid_extra_keys = forbid_extra_keys
|
||||
self.type_overrides = dict(type_overrides)
|
||||
self.use_alias = use_alias
|
||||
|
||||
unstruct_collection_overrides = {
|
||||
get_origin(k) or k: v for k, v in unstruct_collection_overrides.items()
|
||||
@@ -1246,7 +1263,7 @@ class Converter(BaseConverter):
|
||||
attribs = fields(origin or cl)
|
||||
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
|
||||
# PEP 563 annotations - need to be resolved.
|
||||
resolve_types(cl)
|
||||
resolve_types(origin or cl)
|
||||
attrib_overrides = {
|
||||
a.name: self.type_overrides[a.type]
|
||||
for a in attribs
|
||||
@@ -1284,10 +1301,11 @@ class Converter(BaseConverter):
|
||||
def gen_structure_attrs_fromdict(
|
||||
self, cl: type[T]
|
||||
) -> Callable[[Mapping[str, Any], Any], T]:
|
||||
attribs = fields(get_origin(cl) or cl if is_generic(cl) else cl)
|
||||
origin = get_origin(cl)
|
||||
attribs = fields(origin or cl if is_generic(cl) else cl)
|
||||
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
|
||||
# PEP 563 annotations - need to be resolved.
|
||||
resolve_types(cl)
|
||||
resolve_types(origin or cl)
|
||||
attrib_overrides = {
|
||||
a.name: self.type_overrides[a.type]
|
||||
for a in attribs
|
||||
@@ -1299,6 +1317,7 @@ class Converter(BaseConverter):
|
||||
_cattrs_forbid_extra_keys=self.forbid_extra_keys,
|
||||
_cattrs_prefer_attrib_converters=self._prefer_attrib_converters,
|
||||
_cattrs_detailed_validation=self.detailed_validation,
|
||||
_cattrs_use_alias=self.use_alias,
|
||||
**attrib_overrides,
|
||||
)
|
||||
|
||||
@@ -1377,6 +1396,7 @@ class Converter(BaseConverter):
|
||||
unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None,
|
||||
prefer_attrib_converters: bool | None = None,
|
||||
detailed_validation: bool | None = None,
|
||||
use_alias: bool | None = None,
|
||||
) -> Self:
|
||||
"""Create a copy of the converter, keeping all existing custom hooks.
|
||||
|
||||
@@ -1416,6 +1436,7 @@ class Converter(BaseConverter):
|
||||
if detailed_validation is not None
|
||||
else self.detailed_validation
|
||||
),
|
||||
use_alias=(use_alias if use_alias is not None else self.use_alias),
|
||||
)
|
||||
|
||||
self._unstructure_func.copy_to(
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections.abc import Mapping
|
||||
from dataclasses import MISSING
|
||||
from functools import reduce
|
||||
from operator import or_
|
||||
from typing import TYPE_CHECKING, Any, Callable, Literal, Union
|
||||
from typing import TYPE_CHECKING, Any, Callable, Literal, Union, get_origin
|
||||
|
||||
from attrs import NOTHING, Attribute, AttrsInstance
|
||||
|
||||
@@ -16,7 +16,6 @@ from ._compat import (
|
||||
adapted_fields,
|
||||
fields_dict,
|
||||
get_args,
|
||||
get_origin,
|
||||
has,
|
||||
is_literal,
|
||||
is_union_type,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .converters import BaseConverter
|
||||
|
||||
|
||||
def enum_unstructure_factory(
|
||||
type: type[Enum], converter: "BaseConverter"
|
||||
) -> Callable[[Enum], Any]:
|
||||
"""A factory for generating enum unstructure hooks.
|
||||
|
||||
If the enum is a typed enum (has `_value_`), we use the underlying value's hook.
|
||||
Otherwise, we use the value directly.
|
||||
"""
|
||||
if "_value_" in type.__annotations__:
|
||||
return lambda e: converter.unstructure(e.value)
|
||||
|
||||
return lambda e: e.value
|
||||
|
||||
|
||||
def enum_structure_factory(
|
||||
type: type[Enum], converter: "BaseConverter"
|
||||
) -> Callable[[Any, type[Enum]], Enum]:
|
||||
"""A factory for generating enum structure hooks.
|
||||
|
||||
If the enum is a typed enum (has `_value_`), we structure the value first.
|
||||
Otherwise, we use the value directly.
|
||||
"""
|
||||
if "_value_" in type.__annotations__:
|
||||
val_type = type.__annotations__["_value_"]
|
||||
val_hook = converter.get_structure_hook(val_type)
|
||||
return lambda v, _: type(val_hook(v, val_type))
|
||||
|
||||
return lambda v, _: type(v)
|
||||
@@ -13,14 +13,18 @@ class StructureHandlerNotFoundError(Exception):
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, type_: type) -> None:
|
||||
super().__init__(message)
|
||||
super().__init__(message, type_)
|
||||
self.message = message
|
||||
self.type_ = type_
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class BaseValidationError(ExceptionGroup):
|
||||
cl: type
|
||||
|
||||
def __new__(cls, message: str, excs: Sequence[Exception], cl: type):
|
||||
def __new__(cls, message: str, excs: Sequence[Exception], cl: type) -> Self:
|
||||
obj = super().__new__(cls, message, excs)
|
||||
obj.cl = cl
|
||||
return obj
|
||||
@@ -35,9 +39,7 @@ class IterableValidationNote(str):
|
||||
index: Union[int, str] # Ints for list indices, strs for dict keys
|
||||
type: Any
|
||||
|
||||
def __new__(
|
||||
cls, string: str, index: Union[int, str], type: Any
|
||||
) -> "IterableValidationNote":
|
||||
def __new__(cls, string: str, index: Union[int, str], type: Any) -> Self:
|
||||
instance = str.__new__(cls, string)
|
||||
instance.index = index
|
||||
instance.type = type
|
||||
@@ -76,7 +78,7 @@ class AttributeValidationNote(str):
|
||||
name: str
|
||||
type: Any
|
||||
|
||||
def __new__(cls, string: str, name: str, type: Any) -> "AttributeValidationNote":
|
||||
def __new__(cls, string: str, name: str, type: Any) -> Self:
|
||||
instance = str.__new__(cls, string)
|
||||
instance.name = name
|
||||
instance.type = type
|
||||
@@ -122,11 +124,15 @@ class ForbiddenExtraKeysError(Exception):
|
||||
def __init__(
|
||||
self, message: Optional[str], cl: type, extra_fields: set[str]
|
||||
) -> None:
|
||||
self.message = message
|
||||
self.cl = cl
|
||||
self.extra_fields = extra_fields
|
||||
cln = cl.__name__
|
||||
|
||||
super().__init__(
|
||||
message
|
||||
or f"Extra fields in constructor for {cln}: {', '.join(extra_fields)}"
|
||||
super().__init__(message, cl, extra_fields)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
self.message
|
||||
or f"Extra fields in constructor for {self.cl.__name__}: "
|
||||
f"{', '.join(sorted(self.extra_fields))}"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar
|
||||
|
||||
from attrs import NOTHING, Attribute, Factory
|
||||
from attrs import NOTHING, Attribute, Converter, Factory, evolve
|
||||
from typing_extensions import NoDefault
|
||||
|
||||
from .._compat import (
|
||||
@@ -33,7 +33,7 @@ from ..types import SimpleStructureHook
|
||||
from ._consts import AttributeOverride, already_generating, neutral
|
||||
from ._generics import generate_mapping
|
||||
from ._lc import generate_unique_filename
|
||||
from ._shared import find_structure_handler
|
||||
from ._shared import _annotated_override_or_default, find_structure_handler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..converters import BaseConverter
|
||||
@@ -74,7 +74,7 @@ def make_dict_unstructure_fn_from_attrs(
|
||||
typevar_map: dict[str, Any] = {},
|
||||
_cattrs_omit_if_default: bool = False,
|
||||
_cattrs_use_linecache: bool = True,
|
||||
_cattrs_use_alias: bool = False,
|
||||
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_include_init_false: bool = False,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> Callable[[T], dict[str, Any]]:
|
||||
@@ -95,7 +95,17 @@ def make_dict_unstructure_fn_from_attrs(
|
||||
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
|
||||
will be included.
|
||||
|
||||
.. versionadded:: 24.1.0
|
||||
.. versionadded:: 24.1.0
|
||||
.. versionchanged:: 25.2.0
|
||||
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||
by default.
|
||||
.. versionchanged:: 26.1.0
|
||||
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||
unstructuring.
|
||||
.. versionchanged:: 26.1.0
|
||||
When `_cattrs_omit_if_default` is true and the attribute has an attrs converter
|
||||
specified, the converter is applied to the default value before checking if it
|
||||
is equal to the attribute's value.
|
||||
"""
|
||||
|
||||
fn_name = "unstructure_" + cl.__name__
|
||||
@@ -104,15 +114,27 @@ def make_dict_unstructure_fn_from_attrs(
|
||||
invocation_lines = []
|
||||
internal_arg_parts = {}
|
||||
|
||||
if _cattrs_use_alias == "from_converter":
|
||||
# BaseConverter doesn't have it so we're careful.
|
||||
_cattrs_use_alias = getattr(converter, "use_alias", False)
|
||||
|
||||
for a in attrs:
|
||||
attr_name = a.name
|
||||
override = kwargs.get(attr_name, neutral)
|
||||
if attr_name in kwargs:
|
||||
override = kwargs[attr_name]
|
||||
else:
|
||||
override = _annotated_override_or_default(a.type, neutral)
|
||||
if override != neutral:
|
||||
kwargs[attr_name] = override
|
||||
|
||||
if override.omit:
|
||||
continue
|
||||
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
||||
continue
|
||||
if override.rename is None:
|
||||
kn = attr_name if not _cattrs_use_alias else a.alias
|
||||
if kn != attr_name:
|
||||
kwargs[attr_name] = evolve(override, rename=kn)
|
||||
else:
|
||||
kn = override.rename
|
||||
d = a.default
|
||||
@@ -170,16 +192,32 @@ def make_dict_unstructure_fn_from_attrs(
|
||||
if isinstance(d, Factory):
|
||||
globs[def_name] = d.factory
|
||||
internal_arg_parts[def_name] = d.factory
|
||||
if d.takes_self:
|
||||
lines.append(f" if instance.{attr_name} != {def_name}(instance):")
|
||||
else:
|
||||
lines.append(f" if instance.{attr_name} != {def_name}():")
|
||||
lines.append(f" res['{kn}'] = {invoke}")
|
||||
def_str = f"{def_name}(instance)" if d.takes_self else f"{def_name}()"
|
||||
else:
|
||||
globs[def_name] = d
|
||||
internal_arg_parts[def_name] = d
|
||||
lines.append(f" if instance.{attr_name} != {def_name}:")
|
||||
lines.append(f" res['{kn}'] = {invoke}")
|
||||
def_str = def_name
|
||||
|
||||
c = a.converter
|
||||
if c is not None:
|
||||
conv_name = f"__c_conv_{attr_name}"
|
||||
if isinstance(c, Converter):
|
||||
globs[conv_name] = c
|
||||
internal_arg_parts[conv_name] = c
|
||||
field_name = f"__c_field_{attr_name}"
|
||||
globs[field_name] = a
|
||||
internal_arg_parts[field_name] = a
|
||||
def_str = f"{conv_name}({def_str}, instance, {field_name})"
|
||||
elif isinstance(d, Factory):
|
||||
globs[conv_name] = c
|
||||
internal_arg_parts[conv_name] = c
|
||||
def_str = f"{conv_name}({def_str})"
|
||||
else:
|
||||
globs[def_name] = c(d)
|
||||
internal_arg_parts[def_name] = c(d)
|
||||
|
||||
lines.append(f" if instance.{attr_name} != {def_str}:")
|
||||
lines.append(f" res['{kn}'] = {invoke}")
|
||||
|
||||
else:
|
||||
# No default or no override.
|
||||
@@ -217,7 +255,7 @@ def make_dict_unstructure_fn(
|
||||
converter: BaseConverter,
|
||||
_cattrs_omit_if_default: bool = False,
|
||||
_cattrs_use_linecache: bool = True,
|
||||
_cattrs_use_alias: bool = False,
|
||||
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_include_init_false: bool = False,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> Callable[[T], dict[str, Any]]:
|
||||
@@ -235,13 +273,22 @@ def make_dict_unstructure_fn(
|
||||
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
|
||||
will be included.
|
||||
|
||||
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
||||
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
||||
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
||||
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
||||
.. versionchanged:: 25.2.0
|
||||
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||
by default.
|
||||
.. versionchanged:: 26.1.0
|
||||
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||
unstructuring.
|
||||
"""
|
||||
origin = get_origin(cl)
|
||||
attrs = adapted_fields(origin or cl) # type: ignore
|
||||
|
||||
mapping = {}
|
||||
if _cattrs_use_alias == "from_converter":
|
||||
# BaseConverter doesn't have it so we're careful.
|
||||
_cattrs_use_alias = getattr(converter, "use_alias", False)
|
||||
if is_generic(cl):
|
||||
mapping = generate_mapping(cl, mapping)
|
||||
|
||||
@@ -289,7 +336,7 @@ def make_dict_structure_fn_from_attrs(
|
||||
bool | Literal["from_converter"]
|
||||
) = "from_converter",
|
||||
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_use_alias: bool = False,
|
||||
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_include_init_false: bool = False,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> SimpleStructureHook[Mapping[str, Any], T]:
|
||||
@@ -314,7 +361,13 @@ def make_dict_structure_fn_from_attrs(
|
||||
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
|
||||
will be included.
|
||||
|
||||
.. versionadded:: 24.1.0
|
||||
.. versionadded:: 24.1.0
|
||||
.. versionchanged:: 25.2.0
|
||||
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||
by default.
|
||||
.. versionchanged:: 26.1.0
|
||||
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||
unstructuring.
|
||||
"""
|
||||
|
||||
cl_name = cl.__name__
|
||||
@@ -350,6 +403,9 @@ def make_dict_structure_fn_from_attrs(
|
||||
if _cattrs_forbid_extra_keys == "from_converter":
|
||||
# BaseConverter doesn't have it so we're careful.
|
||||
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
|
||||
if _cattrs_use_alias == "from_converter":
|
||||
# BaseConverter doesn't have it so we're careful.
|
||||
_cattrs_use_alias = getattr(converter, "use_alias", False)
|
||||
if _cattrs_detailed_validation == "from_converter":
|
||||
_cattrs_detailed_validation = converter.detailed_validation
|
||||
if _cattrs_prefer_attrib_converters == "from_converter":
|
||||
@@ -367,7 +423,13 @@ def make_dict_structure_fn_from_attrs(
|
||||
internal_arg_parts["__c_avn"] = AttributeValidationNote
|
||||
for a in attrs:
|
||||
an = a.name
|
||||
override = kwargs.get(an, neutral)
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(a.type, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
|
||||
if override.omit:
|
||||
continue
|
||||
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
||||
@@ -396,6 +458,8 @@ def make_dict_structure_fn_from_attrs(
|
||||
ian = a.alias
|
||||
if override.rename is None:
|
||||
kn = an if not _cattrs_use_alias else a.alias
|
||||
if kn != an:
|
||||
kwargs[an] = evolve(override, rename=kn)
|
||||
else:
|
||||
kn = override.rename
|
||||
|
||||
@@ -496,14 +560,24 @@ def make_dict_structure_fn_from_attrs(
|
||||
# The first loop deals with required args.
|
||||
for a in attrs:
|
||||
an = a.name
|
||||
override = kwargs.get(an, neutral)
|
||||
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(a.type, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
|
||||
if override.omit:
|
||||
continue
|
||||
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
||||
continue
|
||||
|
||||
if a.default is not NOTHING:
|
||||
non_required.append(a)
|
||||
# The next loop will handle it.
|
||||
continue
|
||||
|
||||
t = a.type
|
||||
if isinstance(t, TypeVar):
|
||||
t = typevar_map.get(t.__name__, t)
|
||||
@@ -523,6 +597,8 @@ def make_dict_structure_fn_from_attrs(
|
||||
|
||||
if override.rename is None:
|
||||
kn = an if not _cattrs_use_alias else a.alias
|
||||
if kn != an:
|
||||
kwargs[an] = evolve(override, rename=kn)
|
||||
else:
|
||||
kn = override.rename
|
||||
allowed_fields.add(kn)
|
||||
@@ -592,6 +668,8 @@ def make_dict_structure_fn_from_attrs(
|
||||
|
||||
if override.rename is None:
|
||||
kn = an if not _cattrs_use_alias else a.alias
|
||||
if kn != an:
|
||||
kwargs[an] = evolve(override, rename=kn)
|
||||
else:
|
||||
kn = override.rename
|
||||
allowed_fields.add(kn)
|
||||
@@ -682,7 +760,7 @@ def make_dict_structure_fn(
|
||||
bool | Literal["from_converter"]
|
||||
) = "from_converter",
|
||||
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_use_alias: bool = False,
|
||||
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_include_init_false: bool = False,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> SimpleStructureHook[Mapping[str, Any], T]:
|
||||
@@ -706,14 +784,20 @@ def make_dict_structure_fn(
|
||||
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
|
||||
will be included.
|
||||
|
||||
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
||||
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
||||
.. versionchanged:: 23.2.0
|
||||
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
||||
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
||||
.. versionchanged:: 23.2.0
|
||||
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
|
||||
take their values from the given converter by default.
|
||||
.. versionchanged:: 24.1.0
|
||||
.. versionchanged:: 24.1.0
|
||||
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
|
||||
converter by default.
|
||||
.. versionchanged:: 25.2.0
|
||||
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||
by default.
|
||||
.. versionchanged:: 26.1.0
|
||||
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||
unstructuring.
|
||||
"""
|
||||
|
||||
mapping = {}
|
||||
|
||||
@@ -4,15 +4,31 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from attrs import NOTHING, Attribute, Factory
|
||||
|
||||
from .._compat import is_bare_final
|
||||
from .._compat import get_args, is_annotated, is_bare_final
|
||||
from ..dispatch import StructureHook
|
||||
from ..errors import StructureHandlerNotFoundError
|
||||
from ..fns import raise_error
|
||||
from ._consts import AttributeOverride
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..converters import BaseConverter
|
||||
|
||||
|
||||
def _annotated_override_or_default(
|
||||
type: Any, default: AttributeOverride
|
||||
) -> AttributeOverride:
|
||||
"""
|
||||
If the type is Annotated containing an AttributeOverride, return it.
|
||||
Otherwise, return the default.
|
||||
"""
|
||||
if is_annotated(type):
|
||||
for arg in get_args(type):
|
||||
if isinstance(arg, AttributeOverride):
|
||||
return arg
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def find_structure_handler(
|
||||
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
|
||||
) -> StructureHook | None:
|
||||
|
||||
@@ -3,23 +3,12 @@ from __future__ import annotations
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from inspect import get_annotations
|
||||
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
|
||||
|
||||
from attrs import NOTHING, Attribute
|
||||
from typing_extensions import _TypedDictMeta
|
||||
|
||||
try:
|
||||
from inspect import get_annotations
|
||||
|
||||
def get_annots(cl) -> dict[str, Any]:
|
||||
return get_annotations(cl, eval_str=True)
|
||||
|
||||
except ImportError:
|
||||
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
|
||||
def get_annots(cl) -> dict[str, Any]:
|
||||
return cl.__dict__.get("__annotations__", {})
|
||||
|
||||
|
||||
from .._compat import (
|
||||
get_full_type_hints,
|
||||
get_notrequired_base,
|
||||
@@ -40,7 +29,7 @@ from . import AttributeOverride
|
||||
from ._consts import already_generating, neutral
|
||||
from ._generics import generate_mapping
|
||||
from ._lc import generate_unique_filename
|
||||
from ._shared import find_structure_handler
|
||||
from ._shared import _annotated_override_or_default, find_structure_handler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..converters import BaseConverter
|
||||
@@ -50,6 +39,10 @@ __all__ = ["make_dict_structure_fn", "make_dict_unstructure_fn"]
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def get_annots(cl) -> dict[str, Any]:
|
||||
return get_annotations(cl, eval_str=True)
|
||||
|
||||
|
||||
def make_dict_unstructure_fn(
|
||||
cl: type[T],
|
||||
converter: BaseConverter,
|
||||
@@ -109,11 +102,20 @@ def make_dict_unstructure_fn(
|
||||
# * all attributes resolve to `converter._unstructure_identity`
|
||||
for a in attrs:
|
||||
attr_name = a.name
|
||||
override = kwargs.get(attr_name, neutral)
|
||||
t = a.type
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if attr_name in kwargs:
|
||||
override = kwargs[attr_name]
|
||||
else:
|
||||
override = _annotated_override_or_default(t, neutral)
|
||||
if override != neutral:
|
||||
kwargs[attr_name] = override
|
||||
if override != neutral:
|
||||
break
|
||||
handler = None
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
if t.__name__ in mapping:
|
||||
@@ -125,9 +127,6 @@ def make_dict_unstructure_fn(
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
if handler is None:
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
try:
|
||||
handler = converter.get_unstructure_hook(t)
|
||||
except RecursionError:
|
||||
@@ -142,10 +141,22 @@ def make_dict_unstructure_fn(
|
||||
|
||||
for ix, a in enumerate(attrs):
|
||||
attr_name = a.name
|
||||
override = kwargs.get(attr_name, neutral)
|
||||
t = a.type
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if attr_name in kwargs:
|
||||
override = kwargs[attr_name]
|
||||
else:
|
||||
override = _annotated_override_or_default(t, neutral)
|
||||
if override != neutral:
|
||||
kwargs[attr_name] = override
|
||||
|
||||
if override.omit:
|
||||
lines.append(f" res.pop('{attr_name}', None)")
|
||||
continue
|
||||
|
||||
if override.rename is not None:
|
||||
# We also need to pop when renaming, since we're copying
|
||||
# the original.
|
||||
@@ -160,8 +171,6 @@ def make_dict_unstructure_fn(
|
||||
if override.unstruct_hook is not None:
|
||||
handler = override.unstruct_hook
|
||||
else:
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
if t.__name__ in mapping:
|
||||
t = mapping[t.__name__]
|
||||
@@ -171,9 +180,6 @@ def make_dict_unstructure_fn(
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
if handler is None:
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
try:
|
||||
handler = converter.get_unstructure_hook(t)
|
||||
except RecursionError:
|
||||
@@ -220,12 +226,15 @@ def make_dict_unstructure_fn(
|
||||
)
|
||||
|
||||
eval(compile(script, fname, "exec"), globs)
|
||||
|
||||
res = globs[fn_name]
|
||||
res.overrides = kwargs
|
||||
finally:
|
||||
working_set.remove(cl)
|
||||
if not working_set:
|
||||
del already_generating.working_set
|
||||
|
||||
return globs[fn_name]
|
||||
return res
|
||||
|
||||
|
||||
def make_dict_structure_fn(
|
||||
@@ -326,20 +335,25 @@ def make_dict_structure_fn(
|
||||
for ix, a in enumerate(attrs):
|
||||
an = a.name
|
||||
attr_required = an in req_keys
|
||||
override = kwargs.get(an, neutral)
|
||||
t = a.type
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(t, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
if override.omit:
|
||||
continue
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
t = mapping.get(t.__name__, t)
|
||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
@@ -399,7 +413,12 @@ def make_dict_structure_fn(
|
||||
for ix, a in enumerate(attrs):
|
||||
an = a.name
|
||||
attr_required = an in req_keys
|
||||
override = kwargs.get(an, neutral)
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(a.type, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
if override.omit:
|
||||
continue
|
||||
if not attr_required:
|
||||
@@ -448,13 +467,18 @@ def make_dict_structure_fn(
|
||||
if non_required:
|
||||
for ix, a in non_required:
|
||||
an = a.name
|
||||
override = kwargs.get(an, neutral)
|
||||
t = a.type
|
||||
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(t, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
t = mapping.get(t.__name__, t)
|
||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
@@ -514,7 +538,9 @@ def make_dict_structure_fn(
|
||||
)
|
||||
|
||||
eval(compile(script, fname, "exec"), globs)
|
||||
return globs[fn_name]
|
||||
res = globs[fn_name]
|
||||
res.overrides = kwargs
|
||||
return res
|
||||
|
||||
|
||||
def _adapted_fields(cls: Any) -> list[Attribute]:
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, TypeVar, get_args
|
||||
from typing import Any, ParamSpec, TypeVar, get_args
|
||||
|
||||
from .._compat import is_subclass
|
||||
from ..converters import Converter, UnstructureHook
|
||||
from ..fns import identity
|
||||
|
||||
if sys.version_info[:2] < (3, 10):
|
||||
from typing_extensions import ParamSpec
|
||||
else:
|
||||
from typing import ParamSpec
|
||||
|
||||
|
||||
def validate_datetime(v, _):
|
||||
if not isinstance(v, datetime):
|
||||
|
||||
@@ -99,11 +99,11 @@ def configure_converter(converter: BaseConverter):
|
||||
|
||||
# datetime inherits from date, so identity unstructure hook used
|
||||
# here to prevent the date unstructure hook running.
|
||||
converter.register_unstructure_hook(datetime, lambda v: v)
|
||||
converter.register_unstructure_hook(datetime, identity)
|
||||
converter.register_structure_hook(datetime, validate_datetime)
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ def configure_converter(converter: BaseConverter):
|
||||
)
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ def configure_converter(converter: BaseConverter) -> None:
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda _: identity)
|
||||
configure_union_passthrough(Union[str, bool, int, float, None], converter)
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ def configure_converter(converter: BaseConverter) -> None:
|
||||
converter.register_structure_hook(
|
||||
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
|
||||
)
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
|
||||
@@ -3,18 +3,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from base64 import b64decode
|
||||
from collections.abc import Callable
|
||||
from dataclasses import is_dataclass
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from typing import Any, Callable, TypeVar, Union, get_type_hints
|
||||
from typing import Any, TypeVar, Union, get_type_hints
|
||||
|
||||
from attrs import has as attrs_has
|
||||
from attrs import resolve_types
|
||||
from msgspec import Struct, convert, to_builtins
|
||||
from msgspec.json import Encoder, decode
|
||||
|
||||
from .._compat import fields, get_args, get_origin, is_bare, is_mapping, is_sequence
|
||||
from .._compat import (
|
||||
fields,
|
||||
get_args,
|
||||
get_origin,
|
||||
is_bare,
|
||||
is_mapping,
|
||||
is_sequence,
|
||||
is_subclass,
|
||||
)
|
||||
from ..cols import is_namedtuple
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..dispatch import UnstructureHook
|
||||
@@ -74,7 +83,9 @@ def configure_converter(converter: Converter) -> None:
|
||||
configure_passthroughs(converter)
|
||||
|
||||
converter.register_unstructure_hook(Struct, to_builtins)
|
||||
converter.register_unstructure_hook(Enum, identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
lambda t: is_subclass(t, Enum), lambda t, c: identity
|
||||
)
|
||||
|
||||
converter.register_structure_hook(Struct, convert)
|
||||
converter.register_structure_hook(bytes, lambda v, _: b64decode(v))
|
||||
|
||||
@@ -87,8 +87,8 @@ def configure_converter(converter: Converter) -> None:
|
||||
),
|
||||
]
|
||||
)
|
||||
converter.register_unstructure_hook_func(
|
||||
partial(is_primitive_enum, include_bare_enums=True), identity
|
||||
converter.register_unstructure_hook_factory(
|
||||
partial(is_primitive_enum, include_bare_enums=True), lambda t: identity
|
||||
)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
|
||||
@@ -12,6 +12,7 @@ from tomlkit.items import Float, Integer, String
|
||||
|
||||
from .._compat import is_mapping, is_subclass
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..fns import identity
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import validate_datetime, wrap
|
||||
|
||||
@@ -37,6 +38,9 @@ def configure_converter(converter: BaseConverter):
|
||||
* sets are serialized as lists
|
||||
* tuples are serializas as lists
|
||||
* mapping keys are coerced into strings when unstructuring
|
||||
|
||||
.. versionchanged:: 26.1.0
|
||||
date objects are now passed through to tomlkit without unstructuring.
|
||||
"""
|
||||
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
||||
converter.register_unstructure_hook(
|
||||
@@ -67,10 +71,12 @@ def configure_converter(converter: BaseConverter):
|
||||
|
||||
# datetime inherits from date, so identity unstructure hook used
|
||||
# here to prevent the date unstructure hook running.
|
||||
converter.register_unstructure_hook(datetime, lambda v: v)
|
||||
converter.register_unstructure_hook(datetime, identity)
|
||||
converter.register_structure_hook(datetime, validate_datetime)
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook(date, identity)
|
||||
converter.register_structure_hook(
|
||||
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
|
||||
)
|
||||
configure_union_passthrough(
|
||||
Union[str, String, bool, int, Integer, float, Float], converter
|
||||
)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Preconfigured converters for tomllib."""
|
||||
|
||||
from base64 import b85decode, b85encode
|
||||
from collections.abc import Set
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from operator import attrgetter
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
try:
|
||||
from tomllib import loads
|
||||
except ImportError:
|
||||
from tomli import loads
|
||||
|
||||
try:
|
||||
from tomli_w import dumps
|
||||
except ImportError: # pragma: nocover
|
||||
dumps = None
|
||||
|
||||
from .._compat import is_mapping, is_subclass
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..fns import identity
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import validate_datetime, wrap
|
||||
|
||||
__all__ = ["TomllibConverter", "configure_converter", "make_converter"]
|
||||
|
||||
T = TypeVar("T")
|
||||
_enum_value_getter = attrgetter("_value_")
|
||||
|
||||
|
||||
class TomllibConverter(Converter):
|
||||
"""A converter subclass specialized for tomllib."""
|
||||
|
||||
if dumps is not None:
|
||||
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str:
|
||||
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
|
||||
|
||||
def loads(self, data: str, cl: type[T], **kwargs: Any) -> T:
|
||||
return self.structure(loads(data, **kwargs), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: BaseConverter):
|
||||
"""
|
||||
Configure the converter for use with the tomllib library.
|
||||
|
||||
* bytes are serialized as base85 strings
|
||||
* sets are serialized as lists
|
||||
* tuples are serializas as lists
|
||||
* mapping keys are coerced into strings when unstructuring
|
||||
* dates and datetimes are left for tomllib to handle
|
||||
"""
|
||||
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
||||
converter.register_unstructure_hook(
|
||||
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
|
||||
)
|
||||
|
||||
@converter.register_unstructure_hook_factory(is_mapping)
|
||||
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
|
||||
key_handler = str
|
||||
args = getattr(cl, "__args__", None)
|
||||
if args:
|
||||
if is_subclass(args[0], str):
|
||||
key_handler = _enum_value_getter if is_subclass(args[0], Enum) else None
|
||||
elif is_subclass(args[0], bytes):
|
||||
|
||||
def key_handler(k: bytes):
|
||||
return b85encode(k).decode("utf8")
|
||||
|
||||
return converter.gen_unstructure_mapping(
|
||||
cl, unstructure_to=unstructure_to, key_handler=key_handler
|
||||
)
|
||||
|
||||
converter.register_unstructure_hook(datetime, identity)
|
||||
converter.register_structure_hook(datetime, validate_datetime)
|
||||
converter.register_unstructure_hook(date, identity)
|
||||
converter.register_structure_hook(
|
||||
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
|
||||
)
|
||||
configure_union_passthrough(Union[str, int, float, bool], converter)
|
||||
|
||||
|
||||
@wrap(TomllibConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> TomllibConverter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
Set: list,
|
||||
tuple: list,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = TomllibConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -47,7 +47,7 @@ def configure_converter(converter: BaseConverter):
|
||||
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
|
||||
@@ -9,21 +9,25 @@ from typing import Any, Callable, TypeVar, Union
|
||||
from ..converters import BaseConverter
|
||||
from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn
|
||||
from ..gen._consts import already_generating
|
||||
from ..subclasses import subclasses
|
||||
|
||||
|
||||
def _make_subclasses_tree(cl: type) -> list[type]:
|
||||
# get class origin for accessing subclasses (see #648 for more info)
|
||||
cls_origin = typing.get_origin(cl) or cl
|
||||
return [cl] + [
|
||||
sscl
|
||||
for scl in cls_origin.__subclasses__()
|
||||
for sscl in _make_subclasses_tree(scl)
|
||||
]
|
||||
|
||||
# Use a dict to deduplicate and keep insertion order.
|
||||
seen = {cl: None}
|
||||
for scl in subclasses(cls_origin):
|
||||
for sscl in _make_subclasses_tree(scl):
|
||||
seen[sscl] = None
|
||||
return list(seen)
|
||||
|
||||
|
||||
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
|
||||
"""Whether the given class has subclasses from `given_subclasses`."""
|
||||
actual = set(cl.__subclasses__())
|
||||
cls_origin = typing.get_origin(cl) or cl
|
||||
actual = set(subclasses(cls_origin))
|
||||
given = set(given_subclasses)
|
||||
return bool(actual & given)
|
||||
|
||||
@@ -68,6 +72,9 @@ def include_subclasses(
|
||||
.. versionchanged:: 24.1.0
|
||||
When overrides are not provided, hooks for individual classes are retrieved from
|
||||
the converter instead of generated with no overrides, using converter defaults.
|
||||
.. versionchanged:: 25.2.0
|
||||
Slotted dataclasses work on Python 3.14 via :func:`cattrs.subclasses.subclasses`,
|
||||
which filters out duplicate classes caused by slotting.
|
||||
"""
|
||||
# Due to https://github.com/python-attrs/attrs/issues/1047
|
||||
collect()
|
||||
@@ -231,7 +238,13 @@ def _include_subclasses_with_union_strategy(
|
||||
return cls is _cl
|
||||
|
||||
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
|
||||
subclasses = tuple([c for c in union_classes if issubclass(c, cl)])
|
||||
subclasses = tuple(
|
||||
[
|
||||
c
|
||||
for c in union_classes
|
||||
if issubclass(typing.get_origin(c) or c, typing.get_origin(cl) or cl)
|
||||
]
|
||||
)
|
||||
if len(subclasses) > 1:
|
||||
u = Union[subclasses] # type: ignore
|
||||
union_strategy(u, converter)
|
||||
|
||||
@@ -52,23 +52,10 @@ def configure_tagged_union(
|
||||
if is_type_alias(union):
|
||||
union = union.__value__
|
||||
args = union.__args__
|
||||
|
||||
tag_to_hook = {}
|
||||
exact_cl_unstruct_hooks = {}
|
||||
for cl in args:
|
||||
tag = tag_generator(cl)
|
||||
struct_handler = converter.get_structure_hook(cl)
|
||||
unstruct_handler = converter.get_unstructure_hook(cl)
|
||||
|
||||
def structure_union_member(val: dict, _cl=cl, _h=struct_handler) -> cl:
|
||||
return _h(val, _cl)
|
||||
|
||||
def unstructure_union_member(val: union, _h=unstruct_handler) -> dict:
|
||||
return _h(val)
|
||||
|
||||
tag_to_hook[tag] = structure_union_member
|
||||
exact_cl_unstruct_hooks[cl] = unstructure_union_member
|
||||
|
||||
cl_to_tag = {cl: tag_generator(cl) for cl in args}
|
||||
cl_to_tag = {}
|
||||
|
||||
if default is not NOTHING:
|
||||
default_handler = converter.get_structure_hook(default)
|
||||
@@ -76,36 +63,9 @@ def configure_tagged_union(
|
||||
def structure_default(val: dict, _cl=default, _h=default_handler):
|
||||
return _h(val, _cl)
|
||||
|
||||
tag_to_hook = defaultdict(lambda: structure_default, tag_to_hook)
|
||||
cl_to_tag = defaultdict(lambda: default, cl_to_tag)
|
||||
tag_to_hook = defaultdict(lambda: structure_default)
|
||||
cl_to_tag = defaultdict(lambda: default)
|
||||
|
||||
def unstructure_tagged_union(
|
||||
val: union,
|
||||
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
|
||||
_cl_to_tag=cl_to_tag,
|
||||
_tag_name=tag_name,
|
||||
) -> dict:
|
||||
res = _exact_cl_unstruct_hooks[val.__class__](val)
|
||||
res[_tag_name] = _cl_to_tag[val.__class__]
|
||||
return res
|
||||
|
||||
if default is NOTHING:
|
||||
if getattr(converter, "forbid_extra_keys", False):
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
val = val.copy()
|
||||
return _tag_to_cl[val.pop(_tag_name)](val)
|
||||
|
||||
else:
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
return _tag_to_cl[val[_tag_name]](val)
|
||||
|
||||
else:
|
||||
if getattr(converter, "forbid_extra_keys", False):
|
||||
|
||||
def structure_tagged_union(
|
||||
@@ -135,11 +95,54 @@ def configure_tagged_union(
|
||||
return _tag_to_hook[val[_tag_name]](val)
|
||||
return _dh(val, _default)
|
||||
|
||||
else:
|
||||
if getattr(converter, "forbid_extra_keys", False):
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
val = val.copy()
|
||||
return _tag_to_cl[val.pop(_tag_name)](val)
|
||||
|
||||
else:
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
return _tag_to_cl[val[_tag_name]](val)
|
||||
|
||||
def unstructure_tagged_union(
|
||||
val: union,
|
||||
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
|
||||
_cl_to_tag=cl_to_tag,
|
||||
_tag_name=tag_name,
|
||||
) -> dict:
|
||||
res = _exact_cl_unstruct_hooks[val.__class__](val)
|
||||
res[_tag_name] = _cl_to_tag[val.__class__]
|
||||
return res
|
||||
|
||||
converter.register_unstructure_hook(union, unstructure_tagged_union)
|
||||
converter.register_structure_hook(union, structure_tagged_union)
|
||||
|
||||
for cl in args:
|
||||
tag = tag_generator(cl)
|
||||
struct_handler = converter.get_structure_hook(cl)
|
||||
unstruct_handler = converter.get_unstructure_hook(cl)
|
||||
|
||||
def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
|
||||
def structure_union_member(val: dict, _cl=cl, _h=struct_handler) -> cl:
|
||||
return _h(val, _cl)
|
||||
|
||||
def unstructure_union_member(val: union, _h=unstruct_handler) -> dict:
|
||||
return _h(val)
|
||||
|
||||
tag_to_hook[tag] = structure_union_member
|
||||
exact_cl_unstruct_hooks[cl] = unstructure_union_member
|
||||
cl_to_tag[cl] = tag
|
||||
|
||||
|
||||
def configure_union_passthrough(
|
||||
union: Any, converter: BaseConverter, accept_ints_as_floats: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Configure the converter to support validating and passing through unions of the
|
||||
provided types and their subsets.
|
||||
@@ -162,7 +165,14 @@ def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
|
||||
If the union contains a class and one or more of its subclasses, the subclasses
|
||||
will also be included when validating the superclass.
|
||||
|
||||
:param accept_ints_as_floats: When set (the default), if the provided union
|
||||
contains both ints and floats, actual unions containing only floats will also accept
|
||||
ints. See https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 23.2.0
|
||||
.. versionchanged:: 25.2.0
|
||||
Introduced the `accept_ints_as_floats` parameter.
|
||||
"""
|
||||
args = set(union.__args__)
|
||||
|
||||
@@ -205,6 +215,16 @@ def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
|
||||
and not is_literal(a)
|
||||
}
|
||||
|
||||
# By default, when floats are part of the union, accept ints too.
|
||||
if (
|
||||
accept_ints_as_floats
|
||||
and int in args
|
||||
and float in args
|
||||
and float in non_literal_classes
|
||||
and int not in non_literal_classes
|
||||
):
|
||||
non_literal_classes.add(int)
|
||||
|
||||
if spillover:
|
||||
spillover_type = (
|
||||
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover))
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info <= (3, 13):
|
||||
|
||||
def subclasses(cls: type) -> list[type]:
|
||||
"""A proxy for `cls.__subclasses__()` on older Pythons."""
|
||||
return cls.__subclasses__()
|
||||
|
||||
else:
|
||||
|
||||
def subclasses(cls: type) -> list[type]:
|
||||
"""A helper for getting subclasses of a class.
|
||||
|
||||
Filters out duplicate subclasses of slot dataclasses and attrs classes.
|
||||
"""
|
||||
return [
|
||||
cl
|
||||
for cl in cls.__subclasses__()
|
||||
if (
|
||||
not (
|
||||
"__slots__" not in cl.__dict__
|
||||
and hasattr(cls, "__dataclass_params__")
|
||||
and cls.__dataclass_params__.slots
|
||||
)
|
||||
and not hasattr(cls, "__attrs_base_of_slotted__")
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class ExceptionGroup(Exception):
|
||||
"""Minimal backport used by bundled libs on Python < 3.11."""
|
||||
|
||||
def __new__(cls, message, exceptions):
|
||||
obj = super().__new__(cls, message)
|
||||
obj.message = message
|
||||
obj.exceptions = tuple(exceptions)
|
||||
return obj
|
||||
|
||||
def __init__(self, message, exceptions):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.exceptions = tuple(exceptions)
|
||||
|
||||
def derive(self, exceptions):
|
||||
return self.__class__(self.message, exceptions)
|
||||
@@ -1 +1 @@
|
||||
pip
|
||||
uv
|
||||
@@ -1,4 +1,4 @@
|
||||
importlib_metadata-6.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
importlib_metadata-6.8.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
importlib_metadata-6.8.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
||||
importlib_metadata-6.8.0.dist-info/METADATA,sha256=X79qGRh7gqvuaL_utK5X-MnwHJuIWke0e3eAx0IiLhc,5067
|
||||
importlib_metadata-6.8.0.dist-info/RECORD,,
|
||||
@@ -6,15 +6,6 @@ importlib_metadata-6.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
|
||||
importlib_metadata-6.8.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
||||
importlib_metadata-6.8.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19
|
||||
importlib_metadata/__init__.py,sha256=EiH0qTKP_6oa6pRGJgPrq0kvjnL3hJ18BJH8VaAYSBA,30749
|
||||
importlib_metadata/__pycache__/__init__.cpython-311.pyc,,
|
||||
importlib_metadata/__pycache__/_adapters.cpython-311.pyc,,
|
||||
importlib_metadata/__pycache__/_collections.cpython-311.pyc,,
|
||||
importlib_metadata/__pycache__/_compat.cpython-311.pyc,,
|
||||
importlib_metadata/__pycache__/_functools.cpython-311.pyc,,
|
||||
importlib_metadata/__pycache__/_itertools.cpython-311.pyc,,
|
||||
importlib_metadata/__pycache__/_meta.cpython-311.pyc,,
|
||||
importlib_metadata/__pycache__/_py39compat.cpython-311.pyc,,
|
||||
importlib_metadata/__pycache__/_text.cpython-311.pyc,,
|
||||
importlib_metadata/_adapters.py,sha256=i8S6Ib1OQjcILA-l4gkzktMZe18TaeUNI49PLRp6OBU,2454
|
||||
importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
|
||||
importlib_metadata/_compat.py,sha256=zhjcWMfA9SNExFVVVBozOYbuiok0A4tdMsNk9ZDZi-A,1554
|
||||
|
||||
@@ -1 +1 @@
|
||||
pip
|
||||
uv
|
||||
@@ -1,15 +1,10 @@
|
||||
lsprotocol-2023.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
lsprotocol-2023.0.1.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
lsprotocol-2023.0.1.dist-info/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
|
||||
lsprotocol-2023.0.1.dist-info/METADATA,sha256=oh7M_V0nCX-lx8MCik5z0_J8Wyd7ApJtdl30wWs4Tb8,2237
|
||||
lsprotocol-2023.0.1.dist-info/RECORD,,
|
||||
lsprotocol-2023.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
lsprotocol-2023.0.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
||||
lsprotocol/__init__.py,sha256=zoT6Do2JtGHGb7pOeKpahg4ocXIsSpyowjhOrhUhx8g,94
|
||||
lsprotocol/__pycache__/__init__.cpython-311.pyc,,
|
||||
lsprotocol/__pycache__/_hooks.cpython-311.pyc,,
|
||||
lsprotocol/__pycache__/converters.cpython-311.pyc,,
|
||||
lsprotocol/__pycache__/types.cpython-311.pyc,,
|
||||
lsprotocol/__pycache__/validators.cpython-311.pyc,,
|
||||
lsprotocol/_hooks.py,sha256=PCTq4Ve_dDd02DMcWQ8afu9gj_oyX0B4nDOomPghqYs,41570
|
||||
lsprotocol/converters.py,sha256=404tQOVoZL31R9CBrDe6Gx9Nok5cRph3glKlnSx00fo,433
|
||||
lsprotocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pip
|
||||
@@ -1,41 +0,0 @@
|
||||
packaging-25.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
packaging-25.0.dist-info/METADATA,sha256=W2EaYJw4_vw9YWv0XSCuyY-31T8kXayp4sMPyFx6woI,3281
|
||||
packaging-25.0.dist-info/RECORD,,
|
||||
packaging-25.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
packaging-25.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||
packaging-25.0.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
|
||||
packaging-25.0.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
|
||||
packaging-25.0.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
|
||||
packaging/__init__.py,sha256=_0cDiPVf2S-bNfVmZguxxzmrIYWlyASxpqph4qsJWUc,494
|
||||
packaging/__pycache__/__init__.cpython-311.pyc,,
|
||||
packaging/__pycache__/_elffile.cpython-311.pyc,,
|
||||
packaging/__pycache__/_manylinux.cpython-311.pyc,,
|
||||
packaging/__pycache__/_musllinux.cpython-311.pyc,,
|
||||
packaging/__pycache__/_parser.cpython-311.pyc,,
|
||||
packaging/__pycache__/_structures.cpython-311.pyc,,
|
||||
packaging/__pycache__/_tokenizer.cpython-311.pyc,,
|
||||
packaging/__pycache__/markers.cpython-311.pyc,,
|
||||
packaging/__pycache__/metadata.cpython-311.pyc,,
|
||||
packaging/__pycache__/requirements.cpython-311.pyc,,
|
||||
packaging/__pycache__/specifiers.cpython-311.pyc,,
|
||||
packaging/__pycache__/tags.cpython-311.pyc,,
|
||||
packaging/__pycache__/utils.cpython-311.pyc,,
|
||||
packaging/__pycache__/version.cpython-311.pyc,,
|
||||
packaging/_elffile.py,sha256=UkrbDtW7aeq3qqoAfU16ojyHZ1xsTvGke_WqMTKAKd0,3286
|
||||
packaging/_manylinux.py,sha256=t4y_-dTOcfr36gLY-ztiOpxxJFGO2ikC11HgfysGxiM,9596
|
||||
packaging/_musllinux.py,sha256=p9ZqNYiOItGee8KcZFeHF_YcdhVwGHdK6r-8lgixvGQ,2694
|
||||
packaging/_parser.py,sha256=gYfnj0pRHflVc4RHZit13KNTyN9iiVcU2RUCGi22BwM,10221
|
||||
packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
|
||||
packaging/_tokenizer.py,sha256=OYzt7qKxylOAJ-q0XyK1qAycyPRYLfMPdGQKRXkZWyI,5310
|
||||
packaging/licenses/__init__.py,sha256=VsK4o27CJXWfTi8r2ybJmsBoCdhpnBWuNrskaCVKP7U,5715
|
||||
packaging/licenses/__pycache__/__init__.cpython-311.pyc,,
|
||||
packaging/licenses/__pycache__/_spdx.cpython-311.pyc,,
|
||||
packaging/licenses/_spdx.py,sha256=oAm1ztPFwlsmCKe7lAAsv_OIOfS1cWDu9bNBkeu-2ns,48398
|
||||
packaging/markers.py,sha256=P0we27jm1xUzgGMJxBjtUFCIWeBxTsMeJTOJ6chZmAY,12049
|
||||
packaging/metadata.py,sha256=8IZErqQQnNm53dZZuYq4FGU4_dpyinMeH1QFBIWIkfE,34739
|
||||
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
packaging/requirements.py,sha256=gYyRSAdbrIyKDY66ugIDUQjRMvxkH2ALioTmX3tnL6o,2947
|
||||
packaging/specifiers.py,sha256=gtPu5DTc-F9baLq3FTGEK6dPhHGCuwwZetaY0PSV2gs,40055
|
||||
packaging/tags.py,sha256=41s97W9Zatrq2Ed7Rc3qeBDaHe8pKKvYq2mGjwahfXk,22745
|
||||
packaging/utils.py,sha256=0F3Hh9OFuRgrhTgGZUl5K22Fv1YP2tZl1z_2gO6kJiA,5050
|
||||
packaging/version.py,sha256=olfyuk_DPbflNkJ4wBWetXQ17c74x3DB501degUv7DY,16676
|
||||
@@ -0,0 +1 @@
|
||||
uv
|
||||
+13
-6
@@ -1,14 +1,13 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: packaging
|
||||
Version: 25.0
|
||||
Version: 26.2
|
||||
Summary: Core utilities for Python packages
|
||||
Author-email: Donald Stufft <donald@stufft.io>
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/x-rst
|
||||
License-Expression: Apache-2.0 OR BSD-2-Clause
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: Apache Software License
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
@@ -18,8 +17,10 @@ Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient
|
||||
Classifier: Typing :: Typed
|
||||
License-File: LICENSE
|
||||
License-File: LICENSE.APACHE
|
||||
@@ -42,7 +43,7 @@ or benefit greatly from having a single shared implementation (eg: :pep:`425`).
|
||||
.. end-intro
|
||||
|
||||
The ``packaging`` project includes the following: version handling, specifiers,
|
||||
markers, requirements, tags, utilities.
|
||||
markers, requirements, tags, metadata, lockfiles, utilities.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
@@ -52,8 +53,14 @@ The `documentation`_ provides information and the API for the following:
|
||||
- Version Handling
|
||||
- Specifiers
|
||||
- Markers
|
||||
- Licenses
|
||||
- Requirements
|
||||
- Metadata
|
||||
- Tags
|
||||
- Lockfiles (pylock)
|
||||
- Direct URL helpers
|
||||
- Dependency groups
|
||||
- Errors
|
||||
- Utilities
|
||||
|
||||
Installation
|
||||
@@ -70,11 +77,11 @@ Discussion
|
||||
|
||||
If you run into bugs, you can file them in our `issue tracker`_.
|
||||
|
||||
You can also join ``#pypa`` on Freenode to ask questions or get involved.
|
||||
|
||||
You can also join discussions on `GitHub Discussions`_ to ask questions or get involved.
|
||||
|
||||
.. _`documentation`: https://packaging.pypa.io/
|
||||
.. _`issue tracker`: https://github.com/pypa/packaging/issues
|
||||
.. _`GitHub Discussions`: https://github.com/pypa/packaging/discussions
|
||||
|
||||
|
||||
Code of Conduct
|
||||
@@ -0,0 +1,29 @@
|
||||
packaging-26.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
packaging-26.2.dist-info/METADATA,sha256=T5y815M0FaR5P3dnyYoralEsgj_IHIczeBVwXyMOyr8,3543
|
||||
packaging-26.2.dist-info/RECORD,,
|
||||
packaging-26.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
packaging-26.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||
packaging-26.2.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
|
||||
packaging-26.2.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
|
||||
packaging-26.2.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
|
||||
packaging/__init__.py,sha256=QhMEdPu2XogrJzV3S0KWS6t7l0I9k8EeDRJl4fnw87s,494
|
||||
packaging/_elffile.py,sha256=-sKkptYqzYw2-x3QByJa5mB4rfPWu1pxkZHRx1WAFCY,3211
|
||||
packaging/_manylinux.py,sha256=Hf6nB0cOrayEs96-p3oIXAgGnFquv20DO5l-o2_Xnv0,9559
|
||||
packaging/_musllinux.py,sha256=Z6swjH3MA7XS3qXnmMN7QPhqP3fnoYI0eQ18e9-HgAE,2707
|
||||
packaging/_parser.py,sha256=Kf2nsDw4c54X82pY8ba4F02Bve6OygGMAjL-Begqcew,11698
|
||||
packaging/_structures.py,sha256=60jRbF78p8z5MKnNd6cAprgOadCJHV0DlmUmRBqFZcs,1109
|
||||
packaging/_tokenizer.py,sha256=tFU2Wr-ZZJdAbkXLEJo7qUQDJaIkfft9DqaifiEND7A,5391
|
||||
packaging/dependency_groups.py,sha256=XZIAVFK9uHG4RCGprmJn3VInUWMesxha_kytJuMO9eY,10218
|
||||
packaging/direct_url.py,sha256=eKmbDiPP1sLV4Mj_kCSZqqknrIyVO9Sr7JpF8KCjp4U,10917
|
||||
packaging/errors.py,sha256=6hfEYXAf8v_IF65-lFadJOMIieBP2xIKtyEXjG1nGIs,2680
|
||||
packaging/licenses/__init__.py,sha256=_Jx0XRiD_58palsWnyLrLuh59ZpGCPIPXLKdZo9OJvQ,7293
|
||||
packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122
|
||||
packaging/markers.py,sha256=8fDIUhAF6YMnCNB5FSiwh9pEIusiFzAF73J-0OB8bTk,17055
|
||||
packaging/metadata.py,sha256=crAh0E3GVGVqPlu6EdRFsaG-Y6UYznTUqjuGKRGPv6c,38770
|
||||
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
packaging/pylock.py,sha256=G_1gncTmDbRLY1jo4VDI9Uw-b5IErh_Q9V_BbVJTmD8,33890
|
||||
packaging/requirements.py,sha256=dd1c9aa1gp5NI6btF6UFRQjPn1nxQXnE_T34yDDTEpc,4383
|
||||
packaging/specifiers.py,sha256=Mfp8avQg0lVot17to9lVKBtZD1FsWBTItoGwFUZ3wtg,71514
|
||||
packaging/tags.py,sha256=NQ1weo69_Sjte3xBZ1I_G63CIgCmaN0C24mz-z3hGYo,34224
|
||||
packaging/utils.py,sha256=M7-JMKic2sP1YtV_8aW7eVGB-x3ADuKCiSrsVeCd2Uo,9848
|
||||
packaging/version.py,sha256=Y1aTtxe3sn2xOMa5BdI85-AcHuybbanOVkEvvSRRC8I,38369
|
||||
@@ -6,7 +6,7 @@ __title__ = "packaging"
|
||||
__summary__ = "Core utilities for Python packages"
|
||||
__uri__ = "https://github.com/pypa/packaging"
|
||||
|
||||
__version__ = "25.0"
|
||||
__version__ = "26.2"
|
||||
|
||||
__author__ = "Donald Stufft and individual contributors"
|
||||
__email__ = "donald@stufft.io"
|
||||
|
||||
@@ -4,7 +4,6 @@ ELF file parser.
|
||||
This provides a class ``ELFFile`` that parses an ELF executable in a similar
|
||||
interface to ``ZipFile``. Only the read interface is implemented.
|
||||
|
||||
Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
|
||||
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
|
||||
"""
|
||||
|
||||
|
||||
@@ -15,6 +15,16 @@ EF_ARM_ABIMASK = 0xFF000000
|
||||
EF_ARM_ABI_VER5 = 0x05000000
|
||||
EF_ARM_ABI_FLOAT_HARD = 0x00000400
|
||||
|
||||
_ALLOWED_ARCHS = {
|
||||
"x86_64",
|
||||
"aarch64",
|
||||
"ppc64",
|
||||
"ppc64le",
|
||||
"s390x",
|
||||
"loongarch64",
|
||||
"riscv64",
|
||||
}
|
||||
|
||||
|
||||
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
|
||||
# as the type for `path` until then.
|
||||
@@ -57,16 +67,7 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
|
||||
return _is_linux_armhf(executable)
|
||||
if "i686" in archs:
|
||||
return _is_linux_i686(executable)
|
||||
allowed_archs = {
|
||||
"x86_64",
|
||||
"aarch64",
|
||||
"ppc64",
|
||||
"ppc64le",
|
||||
"s390x",
|
||||
"loongarch64",
|
||||
"riscv64",
|
||||
}
|
||||
return any(arch in allowed_archs for arch in archs)
|
||||
return any(arch in _ALLOWED_ARCHS for arch in archs)
|
||||
|
||||
|
||||
# If glibc ever changes its major version, we need to know what the last
|
||||
@@ -106,7 +107,7 @@ def _glibc_version_string_ctypes() -> str | None:
|
||||
Fallback implementation of glibc_version_string using ctypes.
|
||||
"""
|
||||
try:
|
||||
import ctypes
|
||||
import ctypes # noqa: PLC0415
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
@@ -150,7 +151,7 @@ def _glibc_version_string() -> str | None:
|
||||
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
|
||||
|
||||
|
||||
def _parse_glibc_version(version_str: str) -> tuple[int, int]:
|
||||
def _parse_glibc_version(version_str: str) -> _GLibCVersion:
|
||||
"""Parse glibc version.
|
||||
|
||||
We use a regexp instead of str.split because we want to discard any
|
||||
@@ -165,15 +166,15 @@ def _parse_glibc_version(version_str: str) -> tuple[int, int]:
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return -1, -1
|
||||
return int(m.group("major")), int(m.group("minor"))
|
||||
return _GLibCVersion(-1, -1)
|
||||
return _GLibCVersion(int(m.group("major")), int(m.group("minor")))
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _get_glibc_version() -> tuple[int, int]:
|
||||
def _get_glibc_version() -> _GLibCVersion:
|
||||
version_str = _glibc_version_string()
|
||||
if version_str is None:
|
||||
return (-1, -1)
|
||||
return _GLibCVersion(-1, -1)
|
||||
return _parse_glibc_version(version_str)
|
||||
|
||||
|
||||
@@ -184,7 +185,7 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
|
||||
return False
|
||||
# Check for presence of _manylinux module.
|
||||
try:
|
||||
import _manylinux
|
||||
import _manylinux # noqa: PLC0415
|
||||
except ImportError:
|
||||
return True
|
||||
if hasattr(_manylinux, "manylinux_compatible"):
|
||||
@@ -192,25 +193,26 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
|
||||
if result is not None:
|
||||
return bool(result)
|
||||
return True
|
||||
if version == _GLibCVersion(2, 5):
|
||||
if hasattr(_manylinux, "manylinux1_compatible"):
|
||||
return bool(_manylinux.manylinux1_compatible)
|
||||
if version == _GLibCVersion(2, 12):
|
||||
if hasattr(_manylinux, "manylinux2010_compatible"):
|
||||
return bool(_manylinux.manylinux2010_compatible)
|
||||
if version == _GLibCVersion(2, 17):
|
||||
if hasattr(_manylinux, "manylinux2014_compatible"):
|
||||
return bool(_manylinux.manylinux2014_compatible)
|
||||
if version == _GLibCVersion(2, 5) and hasattr(_manylinux, "manylinux1_compatible"):
|
||||
return bool(_manylinux.manylinux1_compatible)
|
||||
if version == _GLibCVersion(2, 12) and hasattr(
|
||||
_manylinux, "manylinux2010_compatible"
|
||||
):
|
||||
return bool(_manylinux.manylinux2010_compatible)
|
||||
if version == _GLibCVersion(2, 17) and hasattr(
|
||||
_manylinux, "manylinux2014_compatible"
|
||||
):
|
||||
return bool(_manylinux.manylinux2014_compatible)
|
||||
return True
|
||||
|
||||
|
||||
_LEGACY_MANYLINUX_MAP = {
|
||||
_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {
|
||||
# CentOS 7 w/ glibc 2.17 (PEP 599)
|
||||
(2, 17): "manylinux2014",
|
||||
_GLibCVersion(2, 17): "manylinux2014",
|
||||
# CentOS 6 w/ glibc 2.12 (PEP 571)
|
||||
(2, 12): "manylinux2010",
|
||||
_GLibCVersion(2, 12): "manylinux2010",
|
||||
# CentOS 5 w/ glibc 2.5 (PEP 513)
|
||||
(2, 5): "manylinux1",
|
||||
_GLibCVersion(2, 5): "manylinux1",
|
||||
}
|
||||
|
||||
|
||||
@@ -252,11 +254,9 @@ def platform_tags(archs: Sequence[str]) -> Iterator[str]:
|
||||
min_minor = -1
|
||||
for glibc_minor in range(glibc_max.minor, min_minor, -1):
|
||||
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
|
||||
tag = "manylinux_{}_{}".format(*glibc_version)
|
||||
if _is_compatible(arch, glibc_version):
|
||||
yield f"{tag}_{arch}"
|
||||
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
|
||||
if glibc_version in _LEGACY_MANYLINUX_MAP:
|
||||
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
|
||||
if _is_compatible(arch, glibc_version):
|
||||
yield "manylinux_{}_{}_{}".format(*glibc_version, arch)
|
||||
|
||||
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
|
||||
if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version):
|
||||
yield f"{legacy_tag}_{arch}"
|
||||
|
||||
@@ -49,7 +49,7 @@ def _get_musl_version(executable: str) -> _MuslVersion | None:
|
||||
return None
|
||||
if ld is None or "musl" not in ld:
|
||||
return None
|
||||
proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True)
|
||||
proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True)
|
||||
return _parse_musl_version(proc.stderr)
|
||||
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ the implementation.
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from typing import NamedTuple, Sequence, Tuple, Union
|
||||
from typing import List, Literal, NamedTuple, Sequence, Tuple, Union
|
||||
|
||||
from ._tokenizer import DEFAULT_RULES, Tokenizer
|
||||
|
||||
|
||||
class Node:
|
||||
__slots__ = ("value",)
|
||||
|
||||
def __init__(self, value: str) -> None:
|
||||
self.value = value
|
||||
|
||||
@@ -20,31 +22,66 @@ class Node:
|
||||
return self.value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}('{self}')>"
|
||||
return f"<{self.__class__.__name__}({self.value!r})>"
|
||||
|
||||
def serialize(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def __getstate__(self) -> str:
|
||||
# Return just the value string for compactness and stability.
|
||||
return self.value
|
||||
|
||||
def _restore_value(self, value: object) -> None:
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(
|
||||
f"Cannot restore {self.__class__.__name__} value from {value!r}"
|
||||
)
|
||||
self.value = value
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, str):
|
||||
# New format (26.2+): just the value string.
|
||||
self._restore_value(state)
|
||||
return
|
||||
if isinstance(state, tuple) and len(state) == 2:
|
||||
# Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
|
||||
_, slot_dict = state
|
||||
if isinstance(slot_dict, dict) and "value" in slot_dict:
|
||||
self._restore_value(slot_dict["value"])
|
||||
return
|
||||
if isinstance(state, dict) and "value" in state:
|
||||
# Old format (packaging <= 25.0, no __slots__): plain __dict__.
|
||||
self._restore_value(state["value"])
|
||||
return
|
||||
raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
|
||||
|
||||
|
||||
class Variable(Node):
|
||||
__slots__ = ()
|
||||
|
||||
def serialize(self) -> str:
|
||||
return str(self)
|
||||
|
||||
|
||||
class Value(Node):
|
||||
__slots__ = ()
|
||||
|
||||
def serialize(self) -> str:
|
||||
return f'"{self}"'
|
||||
|
||||
|
||||
class Op(Node):
|
||||
__slots__ = ()
|
||||
|
||||
def serialize(self) -> str:
|
||||
return str(self)
|
||||
|
||||
|
||||
MarkerLogical = Literal["and", "or"]
|
||||
MarkerVar = Union[Variable, Value]
|
||||
MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
|
||||
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
|
||||
MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]]
|
||||
MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]]
|
||||
|
||||
|
||||
class ParsedRequirement(NamedTuple):
|
||||
@@ -111,7 +148,9 @@ def _parse_requirement_details(
|
||||
return (url, specifier, marker)
|
||||
|
||||
marker = _parse_requirement_marker(
|
||||
tokenizer, span_start=url_start, after="URL and whitespace"
|
||||
tokenizer,
|
||||
span_start=url_start,
|
||||
expected="semicolon (after URL and whitespace)",
|
||||
)
|
||||
else:
|
||||
specifier_start = tokenizer.position
|
||||
@@ -124,10 +163,10 @@ def _parse_requirement_details(
|
||||
marker = _parse_requirement_marker(
|
||||
tokenizer,
|
||||
span_start=specifier_start,
|
||||
after=(
|
||||
"version specifier"
|
||||
expected=(
|
||||
"comma (within version specifier), semicolon (after version specifier)"
|
||||
if specifier
|
||||
else "name and no valid version specifier"
|
||||
else "semicolon (after name with no version specifier)"
|
||||
),
|
||||
)
|
||||
|
||||
@@ -135,7 +174,7 @@ def _parse_requirement_details(
|
||||
|
||||
|
||||
def _parse_requirement_marker(
|
||||
tokenizer: Tokenizer, *, span_start: int, after: str
|
||||
tokenizer: Tokenizer, *, span_start: int, expected: str
|
||||
) -> MarkerList:
|
||||
"""
|
||||
requirement_marker = SEMICOLON marker WS?
|
||||
@@ -143,8 +182,9 @@ def _parse_requirement_marker(
|
||||
|
||||
if not tokenizer.check("SEMICOLON"):
|
||||
tokenizer.raise_syntax_error(
|
||||
f"Expected end or semicolon (after {after})",
|
||||
f"Expected {expected} or end",
|
||||
span_start=span_start,
|
||||
span_end=None,
|
||||
)
|
||||
tokenizer.read()
|
||||
|
||||
@@ -307,7 +347,7 @@ def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
|
||||
return (marker_var_left, marker_op, marker_var_right)
|
||||
|
||||
|
||||
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar:
|
||||
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
|
||||
"""
|
||||
marker_var = VARIABLE | QUOTED_STRING
|
||||
"""
|
||||
|
||||
@@ -2,60 +2,32 @@
|
||||
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||
# for complete details.
|
||||
|
||||
"""Backward-compatibility shim for unpickling Version objects serialized before
|
||||
packaging 26.1.
|
||||
|
||||
Old pickles reference ``packaging._structures.InfinityType`` and
|
||||
``packaging._structures.NegativeInfinityType``. This module provides minimal
|
||||
stand-in classes so that ``pickle.loads()`` can resolve those references.
|
||||
The deserialized objects are not used for comparisons — ``Version.__setstate__``
|
||||
discards the stale ``_key`` cache and recomputes it from the core version fields.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class InfinityType:
|
||||
"""Stand-in for the removed ``InfinityType`` used in old comparison keys."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Infinity"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(repr(self))
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __le__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, self.__class__)
|
||||
|
||||
def __gt__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __ge__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __neg__(self: object) -> "NegativeInfinityType":
|
||||
return NegativeInfinity
|
||||
|
||||
|
||||
Infinity = InfinityType()
|
||||
|
||||
|
||||
class NegativeInfinityType:
|
||||
"""Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "-Infinity"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(repr(self))
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __le__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, self.__class__)
|
||||
|
||||
def __gt__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __ge__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self: object) -> InfinityType:
|
||||
return Infinity
|
||||
|
||||
|
||||
Infinity = InfinityType()
|
||||
NegativeInfinity = NegativeInfinityType()
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import contextlib
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, NoReturn
|
||||
from typing import Generator, Mapping, NoReturn
|
||||
|
||||
from .specifiers import Specifier
|
||||
|
||||
@@ -33,16 +33,16 @@ class ParserSyntaxError(Exception):
|
||||
|
||||
def __str__(self) -> str:
|
||||
marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
|
||||
return "\n ".join([self.message, self.source, marker])
|
||||
return f"{self.message}\n {self.source}\n {marker}"
|
||||
|
||||
|
||||
DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
|
||||
"LEFT_PARENTHESIS": r"\(",
|
||||
"RIGHT_PARENTHESIS": r"\)",
|
||||
"LEFT_BRACKET": r"\[",
|
||||
"RIGHT_BRACKET": r"\]",
|
||||
"SEMICOLON": r";",
|
||||
"COMMA": r",",
|
||||
DEFAULT_RULES: dict[str, re.Pattern[str]] = {
|
||||
"LEFT_PARENTHESIS": re.compile(r"\("),
|
||||
"RIGHT_PARENTHESIS": re.compile(r"\)"),
|
||||
"LEFT_BRACKET": re.compile(r"\["),
|
||||
"RIGHT_BRACKET": re.compile(r"\]"),
|
||||
"SEMICOLON": re.compile(r";"),
|
||||
"COMMA": re.compile(r","),
|
||||
"QUOTED_STRING": re.compile(
|
||||
r"""
|
||||
(
|
||||
@@ -53,10 +53,10 @@ DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
|
||||
""",
|
||||
re.VERBOSE,
|
||||
),
|
||||
"OP": r"(===|==|~=|!=|<=|>=|<|>)",
|
||||
"BOOLOP": r"\b(or|and)\b",
|
||||
"IN": r"\bin\b",
|
||||
"NOT": r"\bnot\b",
|
||||
"OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"),
|
||||
"BOOLOP": re.compile(r"\b(or|and)\b"),
|
||||
"IN": re.compile(r"\bin\b"),
|
||||
"NOT": re.compile(r"\bnot\b"),
|
||||
"VARIABLE": re.compile(
|
||||
r"""
|
||||
\b(
|
||||
@@ -75,16 +75,16 @@ DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
|
||||
re.VERBOSE,
|
||||
),
|
||||
"SPECIFIER": re.compile(
|
||||
Specifier._operator_regex_str + Specifier._version_regex_str,
|
||||
Specifier._specifier_regex_str,
|
||||
re.VERBOSE | re.IGNORECASE,
|
||||
),
|
||||
"AT": r"\@",
|
||||
"URL": r"[^ \t]+",
|
||||
"IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b",
|
||||
"VERSION_PREFIX_TRAIL": r"\.\*",
|
||||
"VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*",
|
||||
"WS": r"[ \t]+",
|
||||
"END": r"$",
|
||||
"AT": re.compile(r"\@"),
|
||||
"URL": re.compile(r"[^ \t]+"),
|
||||
"IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"),
|
||||
"VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
|
||||
"VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
|
||||
"WS": re.compile(r"[ \t]+"),
|
||||
"END": re.compile(r"$"),
|
||||
}
|
||||
|
||||
|
||||
@@ -99,12 +99,10 @@ class Tokenizer:
|
||||
self,
|
||||
source: str,
|
||||
*,
|
||||
rules: dict[str, str | re.Pattern[str]],
|
||||
rules: Mapping[str, re.Pattern[str]],
|
||||
) -> None:
|
||||
self.source = source
|
||||
self.rules: dict[str, re.Pattern[str]] = {
|
||||
name: re.compile(pattern) for name, pattern in rules.items()
|
||||
}
|
||||
self.rules = rules
|
||||
self.next_token: Token | None = None
|
||||
self.position = 0
|
||||
|
||||
@@ -174,7 +172,7 @@ class Tokenizer:
|
||||
@contextlib.contextmanager
|
||||
def enclosing_tokens(
|
||||
self, open_token: str, close_token: str, *, around: str
|
||||
) -> Iterator[None]:
|
||||
) -> Generator[None, None, None]:
|
||||
if self.check(open_token):
|
||||
open_position = self.position
|
||||
self.read()
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
from .errors import _ErrorCollector
|
||||
from .requirements import Requirement
|
||||
|
||||
__all__ = [
|
||||
"CyclicDependencyGroup",
|
||||
"DependencyGroupInclude",
|
||||
"DependencyGroupResolver",
|
||||
"DuplicateGroupNames",
|
||||
"InvalidDependencyGroupObject",
|
||||
"resolve_dependency_groups",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
# -----------
|
||||
# Error Types
|
||||
# -----------
|
||||
|
||||
|
||||
class DuplicateGroupNames(ValueError):
|
||||
"""
|
||||
The same dependency groups were defined twice, with different non-normalized names.
|
||||
"""
|
||||
|
||||
|
||||
class CyclicDependencyGroup(ValueError):
|
||||
"""
|
||||
The dependency group includes form a cycle.
|
||||
"""
|
||||
|
||||
def __init__(self, requested_group: str, group: str, include_group: str) -> None:
|
||||
self.requested_group = requested_group
|
||||
self.group = group
|
||||
self.include_group = include_group
|
||||
|
||||
if include_group == group:
|
||||
reason = f"{group} includes itself"
|
||||
else:
|
||||
reason = f"{include_group} -> {group}, {group} -> {include_group}"
|
||||
super().__init__(
|
||||
"Cyclic dependency group include while resolving "
|
||||
f"{requested_group}: {reason}"
|
||||
)
|
||||
|
||||
|
||||
# in the PEP 735 spec, the tables in dependency group lists were described as
|
||||
# "Dependency Object Specifiers", but the only defined type of object was a
|
||||
# "Dependency Group Include" -- hence the naming of this error as "Object"
|
||||
class InvalidDependencyGroupObject(ValueError):
|
||||
"""
|
||||
A member of a dependency group was identified as a dict, but was not in a valid
|
||||
format.
|
||||
"""
|
||||
|
||||
|
||||
# ------------------------
|
||||
# Object Model & Interface
|
||||
# ------------------------
|
||||
|
||||
|
||||
class DependencyGroupInclude:
|
||||
__slots__ = ("include_group",)
|
||||
|
||||
def __init__(self, include_group: str) -> None:
|
||||
"""
|
||||
Initialize a DependencyGroupInclude.
|
||||
|
||||
:param include_group: The name of the group referred to by this include.
|
||||
"""
|
||||
self.include_group = include_group
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.include_group!r})"
|
||||
|
||||
|
||||
class DependencyGroupResolver:
|
||||
"""
|
||||
A resolver for Dependency Group data.
|
||||
|
||||
This class handles caching, name normalization, cycle detection, and other
|
||||
parsing requirements. There are only two public methods for exploring the data:
|
||||
``lookup()`` and ``resolve()``.
|
||||
|
||||
:param dependency_groups: A mapping, as provided via pyproject
|
||||
``[dependency-groups]``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
|
||||
) -> None:
|
||||
errors = _ErrorCollector()
|
||||
|
||||
self.dependency_groups = _normalize_group_names(dependency_groups, errors)
|
||||
|
||||
# a map of group names to parsed data
|
||||
self._parsed_groups: dict[
|
||||
str, tuple[Requirement | DependencyGroupInclude, ...]
|
||||
] = {}
|
||||
# a map of group names to their ancestors, used for cycle detection
|
||||
self._include_graph_ancestors: dict[str, tuple[str, ...]] = {}
|
||||
# a cache of completed resolutions to Requirement lists
|
||||
self._resolve_cache: dict[str, tuple[Requirement, ...]] = {}
|
||||
|
||||
errors.finalize("[dependency-groups] data was invalid")
|
||||
|
||||
def lookup(self, group: str) -> tuple[Requirement | DependencyGroupInclude, ...]:
|
||||
"""
|
||||
Lookup a group name, returning the parsed dependency data for that group.
|
||||
This will not resolve includes.
|
||||
|
||||
:param group: the name of the group to lookup
|
||||
"""
|
||||
group = _normalize_name(group)
|
||||
|
||||
with _ErrorCollector().on_exit(
|
||||
f"[dependency-groups] data for {group!r} was malformed"
|
||||
) as errors:
|
||||
return self._parse_group(group, errors)
|
||||
|
||||
def resolve(self, group: str) -> tuple[Requirement, ...]:
|
||||
"""
|
||||
Resolve a dependency group to a list of requirements.
|
||||
|
||||
:param group: the name of the group to resolve
|
||||
"""
|
||||
group = _normalize_name(group)
|
||||
|
||||
with _ErrorCollector().on_exit(
|
||||
f"[dependency-groups] data for {group!r} was malformed"
|
||||
) as errors:
|
||||
return self._resolve(group, group, errors)
|
||||
|
||||
def _resolve(
|
||||
self, group: str, requested_group: str, errors: _ErrorCollector
|
||||
) -> tuple[Requirement, ...]:
|
||||
"""
|
||||
This is a helper for cached resolution to strings. It preserves the name of the
|
||||
group which the user initially requested in order to present a clearer error in
|
||||
the event that a cycle is detected.
|
||||
|
||||
:param group: The normalized name of the group to resolve.
|
||||
:param requested_group: The group which was used in the original, user-facing
|
||||
request.
|
||||
"""
|
||||
if group in self._resolve_cache:
|
||||
return self._resolve_cache[group]
|
||||
|
||||
parsed = self._parse_group(group, errors)
|
||||
|
||||
resolved_group = []
|
||||
|
||||
for item in parsed:
|
||||
if isinstance(item, Requirement):
|
||||
resolved_group.append(item)
|
||||
elif isinstance(item, DependencyGroupInclude):
|
||||
include_group = _normalize_name(item.include_group)
|
||||
|
||||
# if a group is cyclic, record the error
|
||||
# otherwise, follow the include_group reference
|
||||
#
|
||||
# this allows us to examine all includes in a group, even in the
|
||||
# presence of errors
|
||||
if include_group in self._include_graph_ancestors.get(group, ()):
|
||||
errors.error(
|
||||
CyclicDependencyGroup(
|
||||
requested_group, group, item.include_group
|
||||
)
|
||||
)
|
||||
else:
|
||||
self._include_graph_ancestors[include_group] = (
|
||||
*self._include_graph_ancestors.get(group, ()),
|
||||
group,
|
||||
)
|
||||
resolved_group.extend(
|
||||
self._resolve(include_group, requested_group, errors)
|
||||
)
|
||||
else: # pragma: no cover
|
||||
raise NotImplementedError(
|
||||
f"Invalid dependency group item after parse: {item}"
|
||||
)
|
||||
|
||||
# in the event that errors were detected, present the group as empty and do not
|
||||
# cache the result
|
||||
# this ensures that repeated access to a cyclic group will raise multiple errors
|
||||
if errors.errors:
|
||||
return ()
|
||||
|
||||
self._resolve_cache[group] = tuple(resolved_group)
|
||||
return self._resolve_cache[group]
|
||||
|
||||
def _parse_group(
|
||||
self, group: str, errors: _ErrorCollector
|
||||
) -> tuple[Requirement | DependencyGroupInclude, ...]:
|
||||
# short circuit -- never do the work twice
|
||||
if group in self._parsed_groups:
|
||||
return self._parsed_groups[group]
|
||||
|
||||
if group not in self.dependency_groups:
|
||||
errors.error(LookupError(f"Dependency group '{group}' not found"))
|
||||
return ()
|
||||
|
||||
raw_group = self.dependency_groups[group]
|
||||
if isinstance(raw_group, str):
|
||||
errors.error(
|
||||
TypeError(
|
||||
f"Dependency group {group!r} contained a string rather than a list."
|
||||
)
|
||||
)
|
||||
return ()
|
||||
|
||||
if not isinstance(raw_group, Sequence):
|
||||
errors.error(
|
||||
TypeError(f"Dependency group {group!r} is not a sequence type.")
|
||||
)
|
||||
return ()
|
||||
|
||||
elements: list[Requirement | DependencyGroupInclude] = []
|
||||
for item in raw_group:
|
||||
if isinstance(item, str):
|
||||
# packaging.requirements.Requirement parsing ensures that this is a
|
||||
# valid PEP 508 Dependency Specifier
|
||||
# raises InvalidRequirement on failure
|
||||
elements.append(Requirement(item))
|
||||
elif isinstance(item, Mapping):
|
||||
if tuple(item.keys()) != ("include-group",):
|
||||
errors.error(
|
||||
InvalidDependencyGroupObject(
|
||||
f"Invalid dependency group item: {item!r}"
|
||||
)
|
||||
)
|
||||
else:
|
||||
include_group = item["include-group"]
|
||||
elements.append(DependencyGroupInclude(include_group=include_group))
|
||||
else:
|
||||
errors.error(TypeError(f"Invalid dependency group item: {item!r}"))
|
||||
|
||||
self._parsed_groups[group] = tuple(elements)
|
||||
return self._parsed_groups[group]
|
||||
|
||||
|
||||
# --------------------
|
||||
# Functional Interface
|
||||
# --------------------
|
||||
|
||||
|
||||
def resolve_dependency_groups(
|
||||
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]], /, *groups: str
|
||||
) -> tuple[str, ...]:
|
||||
"""
|
||||
Resolve a dependency group to a tuple of requirements, as strings.
|
||||
|
||||
:param dependency_groups: the parsed contents of the ``[dependency-groups]`` table
|
||||
from ``pyproject.toml``
|
||||
:param groups: the name of the group(s) to resolve
|
||||
"""
|
||||
resolver = DependencyGroupResolver(dependency_groups)
|
||||
return tuple(str(r) for group in groups for r in resolver.resolve(group))
|
||||
|
||||
|
||||
# ----------------
|
||||
# internal helpers
|
||||
# ----------------
|
||||
|
||||
|
||||
_NORMALIZE_PATTERN = re.compile(r"[-_.]+")
|
||||
|
||||
|
||||
def _normalize_name(name: str) -> str:
|
||||
return _NORMALIZE_PATTERN.sub("-", name).lower()
|
||||
|
||||
|
||||
def _normalize_group_names(
|
||||
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
|
||||
errors: _ErrorCollector,
|
||||
) -> dict[str, Sequence[str | Mapping[str, str]]]:
|
||||
original_names: dict[str, list[str]] = {}
|
||||
normalized_groups: dict[str, Sequence[str | Mapping[str, str]]] = {}
|
||||
|
||||
for group_name, value in dependency_groups.items():
|
||||
normed_group_name = _normalize_name(group_name)
|
||||
original_names.setdefault(normed_group_name, []).append(group_name)
|
||||
normalized_groups[normed_group_name] = value
|
||||
|
||||
for normed_name, names in original_names.items():
|
||||
if len(names) > 1:
|
||||
errors.error(
|
||||
DuplicateGroupNames(
|
||||
"Duplicate dependency group names: "
|
||||
f"{normed_name} ({', '.join(names)})"
|
||||
)
|
||||
)
|
||||
|
||||
return normalized_groups
|
||||
@@ -0,0 +1,325 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
import urllib.parse
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import sys
|
||||
from collections.abc import Collection
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self
|
||||
else:
|
||||
from typing_extensions import Self
|
||||
|
||||
__all__ = [
|
||||
"ArchiveInfo",
|
||||
"DirInfo",
|
||||
"DirectUrl",
|
||||
"DirectUrlValidationError",
|
||||
"VcsInfo",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class _FromMappingProtocol(Protocol): # pragma: no cover
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
|
||||
|
||||
|
||||
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
|
||||
|
||||
|
||||
def _json_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||
return {key: value for key, value in data if value is not None}
|
||||
|
||||
|
||||
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
|
||||
"""Get a value from the dictionary and verify it's the expected type."""
|
||||
if (value := d.get(key)) is None:
|
||||
return None
|
||||
if not isinstance(value, expected_type):
|
||||
raise DirectUrlValidationError(
|
||||
f"Unexpected type {type(value).__name__} "
|
||||
f"(expected {expected_type.__name__})",
|
||||
context=key,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
|
||||
"""Get a required value from the dictionary and verify it's the expected type."""
|
||||
if (value := _get(d, expected_type, key)) is None:
|
||||
raise _DirectUrlRequiredKeyError(key)
|
||||
return value
|
||||
|
||||
|
||||
def _get_object(
|
||||
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
|
||||
) -> _FromMappingProtocolT | None:
|
||||
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
|
||||
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
|
||||
return None
|
||||
try:
|
||||
return target_type._from_dict(value)
|
||||
except Exception as e:
|
||||
raise DirectUrlValidationError(e, context=key) from e
|
||||
|
||||
|
||||
_PEP610_USER_PASS_ENV_VARS_REGEX = re.compile(
|
||||
r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$"
|
||||
)
|
||||
|
||||
|
||||
def _strip_auth_from_netloc(netloc: str, safe_user_passwords: Collection[str]) -> str:
|
||||
if "@" not in netloc:
|
||||
return netloc
|
||||
user_pass, netloc_no_user_pass = netloc.split("@", 1)
|
||||
if user_pass in safe_user_passwords:
|
||||
return netloc
|
||||
if _PEP610_USER_PASS_ENV_VARS_REGEX.match(user_pass):
|
||||
return netloc
|
||||
return netloc_no_user_pass
|
||||
|
||||
|
||||
def _strip_url(url: str, safe_user_passwords: Collection[str]) -> str:
|
||||
"""url with user:password part removed unless it is formed with
|
||||
environment variables as specified in PEP 610, or it is a safe user:password
|
||||
such as `git`.
|
||||
"""
|
||||
parsed_url = urllib.parse.urlsplit(url)
|
||||
netloc = _strip_auth_from_netloc(parsed_url.netloc, safe_user_passwords)
|
||||
return urllib.parse.urlunsplit(
|
||||
(
|
||||
parsed_url.scheme,
|
||||
netloc,
|
||||
parsed_url.path,
|
||||
parsed_url.query,
|
||||
parsed_url.fragment,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class DirectUrlValidationError(Exception):
|
||||
"""Raised when when input data is not spec-compliant."""
|
||||
|
||||
context: str | None = None
|
||||
message: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cause: str | Exception,
|
||||
*,
|
||||
context: str | None = None,
|
||||
) -> None:
|
||||
if isinstance(cause, DirectUrlValidationError):
|
||||
if cause.context:
|
||||
self.context = (
|
||||
f"{context}.{cause.context}" if context else cause.context
|
||||
)
|
||||
else:
|
||||
self.context = context # pragma: no cover
|
||||
self.message = cause.message
|
||||
else:
|
||||
self.context = context
|
||||
self.message = str(cause)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.context:
|
||||
return f"{self.message} in {self.context!r}"
|
||||
return self.message
|
||||
|
||||
|
||||
class _DirectUrlRequiredKeyError(DirectUrlValidationError):
|
||||
def __init__(self, key: str) -> None:
|
||||
super().__init__("Missing required value", context=key)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class VcsInfo:
|
||||
vcs: str
|
||||
commit_id: str
|
||||
requested_revision: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
vcs: str,
|
||||
commit_id: str,
|
||||
requested_revision: str | None = None,
|
||||
) -> None:
|
||||
object.__setattr__(self, "vcs", vcs)
|
||||
object.__setattr__(self, "commit_id", commit_id)
|
||||
object.__setattr__(self, "requested_revision", requested_revision)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
# We can't validate vcs value because is not closed.
|
||||
return cls(
|
||||
vcs=_get_required(d, str, "vcs"),
|
||||
requested_revision=_get(d, str, "requested_revision"),
|
||||
commit_id=_get_required(d, str, "commit_id"),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class ArchiveInfo:
|
||||
hashes: Mapping[str, str] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
hashes: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
object.__setattr__(self, "hashes", hashes)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
hashes = _get(d, Mapping, "hashes") # type: ignore[type-abstract]
|
||||
if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):
|
||||
raise DirectUrlValidationError(
|
||||
"Hash values must be strings", context="hashes"
|
||||
)
|
||||
legacy_hash = _get(d, str, "hash")
|
||||
if legacy_hash is not None:
|
||||
if "=" not in legacy_hash:
|
||||
raise DirectUrlValidationError(
|
||||
"Invalid hash format (expected '<algorithm>=<hash>')",
|
||||
context="hash",
|
||||
)
|
||||
hash_algorithm, hash_value = legacy_hash.split("=", 1)
|
||||
if hashes is None:
|
||||
# if `hashes` are not present, we can derive it from the legacy `hash`
|
||||
hashes = {hash_algorithm: hash_value}
|
||||
else:
|
||||
# if `hashes` are present, the legacy `hash` must match one of them
|
||||
if hash_algorithm not in hashes:
|
||||
raise DirectUrlValidationError(
|
||||
f"Algorithm {hash_algorithm!r} used in hash field "
|
||||
f"is not present in hashes field",
|
||||
context="hashes",
|
||||
)
|
||||
if hashes[hash_algorithm] != hash_value:
|
||||
raise DirectUrlValidationError(
|
||||
f"Algorithm {hash_algorithm!r} used in hash field "
|
||||
f"has different value in hashes field",
|
||||
context="hash",
|
||||
)
|
||||
return cls(hashes=hashes)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class DirInfo:
|
||||
editable: bool | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
editable: bool | None = None,
|
||||
) -> None:
|
||||
object.__setattr__(self, "editable", editable)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
return cls(
|
||||
editable=_get(d, bool, "editable"),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class DirectUrl:
|
||||
"""A class representing a direct URL."""
|
||||
|
||||
url: str
|
||||
archive_info: ArchiveInfo | None = None
|
||||
vcs_info: VcsInfo | None = None
|
||||
dir_info: DirInfo | None = None
|
||||
subdirectory: str | None = None # XXX Path or str?
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
url: str,
|
||||
archive_info: ArchiveInfo | None = None,
|
||||
vcs_info: VcsInfo | None = None,
|
||||
dir_info: DirInfo | None = None,
|
||||
subdirectory: str | None = None,
|
||||
) -> None:
|
||||
object.__setattr__(self, "url", url)
|
||||
object.__setattr__(self, "archive_info", archive_info)
|
||||
object.__setattr__(self, "vcs_info", vcs_info)
|
||||
object.__setattr__(self, "dir_info", dir_info)
|
||||
object.__setattr__(self, "subdirectory", subdirectory)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
direct_url = cls(
|
||||
url=_get_required(d, str, "url"),
|
||||
archive_info=_get_object(d, ArchiveInfo, "archive_info"),
|
||||
vcs_info=_get_object(d, VcsInfo, "vcs_info"),
|
||||
dir_info=_get_object(d, DirInfo, "dir_info"),
|
||||
subdirectory=_get(d, str, "subdirectory"),
|
||||
)
|
||||
if (
|
||||
bool(direct_url.vcs_info)
|
||||
+ bool(direct_url.archive_info)
|
||||
+ bool(direct_url.dir_info)
|
||||
) != 1:
|
||||
raise DirectUrlValidationError(
|
||||
"Exactly one of vcs_info, archive_info, dir_info must be present"
|
||||
)
|
||||
if direct_url.dir_info is not None and not direct_url.url.startswith("file://"):
|
||||
raise DirectUrlValidationError(
|
||||
"URL scheme must be file:// when dir_info is present",
|
||||
context="url",
|
||||
)
|
||||
# XXX subdirectory must be relative, can we, should we validate that here?
|
||||
return direct_url
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
|
||||
"""Create and validate a DirectUrl instance from a JSON dictionary."""
|
||||
return cls._from_dict(d)
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
*,
|
||||
generate_legacy_hash: bool = False,
|
||||
strip_user_password: bool = True,
|
||||
safe_user_passwords: Collection[str] = ("git",),
|
||||
) -> Mapping[str, Any]:
|
||||
"""Convert the DirectUrl instance to a JSON dictionary.
|
||||
|
||||
:param generate_legacy_hash: If True, include a legacy `hash` field in
|
||||
`archive_info` for backward compatibility with tools that don't
|
||||
support the `hashes` field.
|
||||
:param strip_user_password: If True, strip user:password from the URL
|
||||
unless it is formed with environment variables as specified in PEP
|
||||
610, or it is a safe user:password such as `git`.
|
||||
:param safe_user_passwords: A collection of user:password strings that
|
||||
should not be stripped from the URL even if `strip_user_password` is
|
||||
True.
|
||||
"""
|
||||
res = dataclasses.asdict(self, dict_factory=_json_dict_factory)
|
||||
if generate_legacy_hash and self.archive_info and self.archive_info.hashes:
|
||||
hash_algorithm, hash_value = next(iter(self.archive_info.hashes.items()))
|
||||
res["archive_info"]["hash"] = f"{hash_algorithm}={hash_value}"
|
||||
if strip_user_password:
|
||||
res["url"] = _strip_url(self.url, safe_user_passwords)
|
||||
return res
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate the DirectUrl instance against the specification.
|
||||
|
||||
Raises :class:`DirectUrlValidationError` if invalid.
|
||||
"""
|
||||
self.from_dict(self.to_dict())
|
||||
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import sys
|
||||
import typing
|
||||
|
||||
__all__ = ["ExceptionGroup"]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11): # pragma: no cover
|
||||
from builtins import ExceptionGroup
|
||||
else: # pragma: no cover
|
||||
|
||||
class ExceptionGroup(Exception):
|
||||
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
|
||||
|
||||
If :external:exc:`ExceptionGroup` is already defined by Python itself,
|
||||
that version is used instead.
|
||||
"""
|
||||
|
||||
message: str
|
||||
exceptions: list[Exception]
|
||||
|
||||
def __init__(self, message: str, exceptions: list[Exception]) -> None:
|
||||
self.message = message
|
||||
self.exceptions = exceptions
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _ErrorCollector:
|
||||
"""
|
||||
Collect errors into ExceptionGroups.
|
||||
|
||||
Used like this:
|
||||
|
||||
collector = _ErrorCollector()
|
||||
# Add a single exception
|
||||
collector.error(ValueError("one"))
|
||||
|
||||
# Supports nesting, including combining ExceptionGroups
|
||||
with collector.collect():
|
||||
raise ValueError("two")
|
||||
collector.finalize("Found some errors")
|
||||
|
||||
Since making a collector and then calling finalize later is a common pattern,
|
||||
a convenience method ``on_exit`` is provided.
|
||||
"""
|
||||
|
||||
errors: list[Exception] = dataclasses.field(default_factory=list, init=False)
|
||||
|
||||
def finalize(self, msg: str) -> None:
|
||||
"""Raise a group exception if there are any errors."""
|
||||
if self.errors:
|
||||
raise ExceptionGroup(msg, self.errors)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector, None, None]:
|
||||
"""
|
||||
Calls finalize if no uncollected errors were present.
|
||||
|
||||
Uncollected errors are raised normally.
|
||||
"""
|
||||
yield self
|
||||
self.finalize(msg)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def collect(self, *err_cls: type[Exception]) -> typing.Generator[None, None, None]:
|
||||
"""
|
||||
Context manager to collect errors into the error list.
|
||||
|
||||
Must be inside loops, as only one error can be collected at a time.
|
||||
"""
|
||||
error_classes = err_cls or (Exception,)
|
||||
try:
|
||||
yield
|
||||
except ExceptionGroup as error:
|
||||
self.errors.extend(error.exceptions)
|
||||
except error_classes as error:
|
||||
self.errors.append(error)
|
||||
|
||||
def error(
|
||||
self,
|
||||
error: Exception,
|
||||
) -> None:
|
||||
"""Add an error to the list."""
|
||||
self.errors.append(error)
|
||||
@@ -34,7 +34,7 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import NewType, cast
|
||||
|
||||
from packaging.licenses._spdx import EXCEPTIONS, LICENSES
|
||||
from ._spdx import EXCEPTIONS, LICENSES
|
||||
|
||||
__all__ = [
|
||||
"InvalidLicenseExpression",
|
||||
@@ -42,14 +42,25 @@ __all__ = [
|
||||
"canonicalize_license_expression",
|
||||
]
|
||||
|
||||
|
||||
# Simple __dir__ implementation since there are no public submodules
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
|
||||
|
||||
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
|
||||
"""
|
||||
A :class:`typing.NewType` of :class:`str`, representing a normalized
|
||||
License-Expression.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidLicenseExpression(ValueError):
|
||||
"""Raised when a license-expression string is invalid
|
||||
|
||||
>>> from packaging.licenses import canonicalize_license_expression
|
||||
>>> canonicalize_license_expression("invalid")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
@@ -60,6 +71,34 @@ class InvalidLicenseExpression(ValueError):
|
||||
def canonicalize_license_expression(
|
||||
raw_license_expression: str,
|
||||
) -> NormalizedLicenseExpression:
|
||||
"""
|
||||
This function takes a valid License-Expression, and returns the normalized
|
||||
form of it.
|
||||
|
||||
The return type is typed as :class:`NormalizedLicenseExpression`. This
|
||||
allows type checkers to help require that a string has passed through this
|
||||
function before use.
|
||||
|
||||
:param str raw_license_expression: The License-Expression to canonicalize.
|
||||
:raises InvalidLicenseExpression: If the License-Expression is invalid due to an
|
||||
invalid/unknown license identifier or invalid syntax.
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from packaging.licenses import canonicalize_license_expression
|
||||
>>> canonicalize_license_expression("mit")
|
||||
'MIT'
|
||||
>>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
|
||||
'MIT AND (Apache-2.0 OR BSD-2-Clause)'
|
||||
>>> canonicalize_license_expression("(mit")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
InvalidLicenseExpression: Invalid license expression: '(mit'
|
||||
>>> canonicalize_license_expression("Use-it-after-midnight")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'
|
||||
"""
|
||||
if not raw_license_expression:
|
||||
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||
raise InvalidLicenseExpression(message)
|
||||
@@ -80,16 +119,21 @@ def canonicalize_license_expression(
|
||||
|
||||
tokens = license_expression.split()
|
||||
|
||||
# Rather than implementing boolean logic, we create an expression that Python can
|
||||
# parse. Everything that is not involved with the grammar itself is treated as
|
||||
# `False` and the expression should evaluate as such.
|
||||
# Rather than implementing a parenthesis/boolean logic parser, create an
|
||||
# expression that Python can parse. Everything that is not involved with the
|
||||
# grammar itself is replaced with the placeholder `False` and the resultant
|
||||
# expression should become a valid Python expression.
|
||||
python_tokens = []
|
||||
for token in tokens:
|
||||
if token not in {"or", "and", "with", "(", ")"}:
|
||||
python_tokens.append("False")
|
||||
elif token == "with":
|
||||
python_tokens.append("or")
|
||||
elif token == "(" and python_tokens and python_tokens[-1] not in {"or", "and"}:
|
||||
elif (
|
||||
token == "("
|
||||
and python_tokens
|
||||
and python_tokens[-1] not in {"or", "and", "("}
|
||||
) or (token == ")" and python_tokens and python_tokens[-1] == "("):
|
||||
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||
raise InvalidLicenseExpression(message)
|
||||
else:
|
||||
@@ -97,11 +141,8 @@ def canonicalize_license_expression(
|
||||
|
||||
python_expression = " ".join(python_tokens)
|
||||
try:
|
||||
invalid = eval(python_expression, globals(), locals())
|
||||
except Exception:
|
||||
invalid = True
|
||||
|
||||
if invalid is not False:
|
||||
compile(python_expression, "", "eval")
|
||||
except SyntaxError:
|
||||
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||
raise InvalidLicenseExpression(message) from None
|
||||
|
||||
@@ -140,6 +181,6 @@ def canonicalize_license_expression(
|
||||
normalized_expression = " ".join(normalized_tokens)
|
||||
|
||||
return cast(
|
||||
NormalizedLicenseExpression,
|
||||
"NormalizedLicenseExpression",
|
||||
normalized_expression.replace("( ", "(").replace(" )", ")"),
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ class SPDXException(TypedDict):
|
||||
deprecated: bool
|
||||
|
||||
|
||||
VERSION = '3.25.0'
|
||||
VERSION = '3.27.0'
|
||||
|
||||
LICENSES: dict[str, SPDXLicense] = {
|
||||
'0bsd': {'id': '0BSD', 'deprecated': False},
|
||||
@@ -46,6 +46,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
|
||||
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
|
||||
'any-osi': {'id': 'any-OSI', 'deprecated': False},
|
||||
'any-osi-perl-modules': {'id': 'any-OSI-perl-modules', 'deprecated': False},
|
||||
'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},
|
||||
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
|
||||
'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},
|
||||
@@ -61,6 +62,8 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
|
||||
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
|
||||
'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},
|
||||
'artistic-dist': {'id': 'Artistic-dist', 'deprecated': False},
|
||||
'aspell-ru': {'id': 'Aspell-RU', 'deprecated': False},
|
||||
'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
|
||||
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
|
||||
'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
|
||||
@@ -75,6 +78,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'blessing': {'id': 'blessing', 'deprecated': False},
|
||||
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
|
||||
'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},
|
||||
'boehm-gc-without-fee': {'id': 'Boehm-GC-without-fee', 'deprecated': False},
|
||||
'borceux': {'id': 'Borceux', 'deprecated': False},
|
||||
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
|
||||
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},
|
||||
@@ -85,6 +89,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
|
||||
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},
|
||||
'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},
|
||||
'bsd-2-clause-pkgconf-disclaimer': {'id': 'BSD-2-Clause-pkgconf-disclaimer', 'deprecated': False},
|
||||
'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
|
||||
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
|
||||
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},
|
||||
@@ -176,6 +181,8 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},
|
||||
'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
|
||||
'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},
|
||||
'cc-pdm-1.0': {'id': 'CC-PDM-1.0', 'deprecated': False},
|
||||
'cc-sa-1.0': {'id': 'CC-SA-1.0', 'deprecated': False},
|
||||
'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
|
||||
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
|
||||
'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},
|
||||
@@ -215,6 +222,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
|
||||
'cronyx': {'id': 'Cronyx', 'deprecated': False},
|
||||
'crossword': {'id': 'Crossword', 'deprecated': False},
|
||||
'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False},
|
||||
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
|
||||
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
|
||||
'cube': {'id': 'Cube', 'deprecated': False},
|
||||
@@ -226,7 +234,9 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
|
||||
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
|
||||
'doc': {'id': 'DOC', 'deprecated': False},
|
||||
'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False},
|
||||
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
|
||||
'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False},
|
||||
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
|
||||
'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
|
||||
'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},
|
||||
@@ -263,12 +273,17 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
|
||||
'fsful': {'id': 'FSFUL', 'deprecated': False},
|
||||
'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
|
||||
'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False},
|
||||
'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},
|
||||
'fsl-1.1-alv2': {'id': 'FSL-1.1-ALv2', 'deprecated': False},
|
||||
'fsl-1.1-mit': {'id': 'FSL-1.1-MIT', 'deprecated': False},
|
||||
'ftl': {'id': 'FTL', 'deprecated': False},
|
||||
'furuseth': {'id': 'Furuseth', 'deprecated': False},
|
||||
'fwlw': {'id': 'fwlw', 'deprecated': False},
|
||||
'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False},
|
||||
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
|
||||
'gd': {'id': 'GD', 'deprecated': False},
|
||||
'generic-xts': {'id': 'generic-xts', 'deprecated': False},
|
||||
'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},
|
||||
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},
|
||||
'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},
|
||||
@@ -320,6 +335,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'gtkbook': {'id': 'gtkbook', 'deprecated': False},
|
||||
'gutmann': {'id': 'Gutmann', 'deprecated': False},
|
||||
'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
|
||||
'hdf5': {'id': 'HDF5', 'deprecated': False},
|
||||
'hdparm': {'id': 'hdparm', 'deprecated': False},
|
||||
'hidapi': {'id': 'HIDAPI', 'deprecated': False},
|
||||
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
|
||||
@@ -360,6 +376,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'imlib2': {'id': 'Imlib2', 'deprecated': False},
|
||||
'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
|
||||
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
|
||||
'innosetup': {'id': 'InnoSetup', 'deprecated': False},
|
||||
'intel': {'id': 'Intel', 'deprecated': False},
|
||||
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
|
||||
'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},
|
||||
@@ -369,6 +386,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
|
||||
'jam': {'id': 'Jam', 'deprecated': False},
|
||||
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
|
||||
'jove': {'id': 'jove', 'deprecated': False},
|
||||
'jpl-image': {'id': 'JPL-image', 'deprecated': False},
|
||||
'jpnic': {'id': 'JPNIC', 'deprecated': False},
|
||||
'json': {'id': 'JSON', 'deprecated': False},
|
||||
@@ -394,6 +412,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
|
||||
'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
|
||||
'libpng': {'id': 'Libpng', 'deprecated': False},
|
||||
'libpng-1.6.35': {'id': 'libpng-1.6.35', 'deprecated': False},
|
||||
'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
|
||||
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
|
||||
'libtiff': {'id': 'libtiff', 'deprecated': False},
|
||||
@@ -424,14 +443,17 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'magaz': {'id': 'magaz', 'deprecated': False},
|
||||
'mailprio': {'id': 'mailprio', 'deprecated': False},
|
||||
'makeindex': {'id': 'MakeIndex', 'deprecated': False},
|
||||
'man2html': {'id': 'man2html', 'deprecated': False},
|
||||
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
|
||||
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
|
||||
'metamail': {'id': 'metamail', 'deprecated': False},
|
||||
'minpack': {'id': 'Minpack', 'deprecated': False},
|
||||
'mips': {'id': 'MIPS', 'deprecated': False},
|
||||
'miros': {'id': 'MirOS', 'deprecated': False},
|
||||
'mit': {'id': 'MIT', 'deprecated': False},
|
||||
'mit-0': {'id': 'MIT-0', 'deprecated': False},
|
||||
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
|
||||
'mit-click': {'id': 'MIT-Click', 'deprecated': False},
|
||||
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
|
||||
'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
|
||||
'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
|
||||
@@ -472,6 +494,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'netcdf': {'id': 'NetCDF', 'deprecated': False},
|
||||
'newsletr': {'id': 'Newsletr', 'deprecated': False},
|
||||
'ngpl': {'id': 'NGPL', 'deprecated': False},
|
||||
'ngrep': {'id': 'ngrep', 'deprecated': False},
|
||||
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
|
||||
'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
|
||||
'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},
|
||||
@@ -486,6 +509,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
|
||||
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
|
||||
'nrl': {'id': 'NRL', 'deprecated': False},
|
||||
'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False},
|
||||
'ntp': {'id': 'NTP', 'deprecated': False},
|
||||
'ntp-0': {'id': 'NTP-0', 'deprecated': False},
|
||||
'nunit': {'id': 'Nunit', 'deprecated': True},
|
||||
@@ -580,6 +604,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'schemereport': {'id': 'SchemeReport', 'deprecated': False},
|
||||
'sendmail': {'id': 'Sendmail', 'deprecated': False},
|
||||
'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},
|
||||
'sendmail-open-source-1.1': {'id': 'Sendmail-Open-Source-1.1', 'deprecated': False},
|
||||
'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},
|
||||
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
|
||||
'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},
|
||||
@@ -592,10 +617,12 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
|
||||
'sl': {'id': 'SL', 'deprecated': False},
|
||||
'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
|
||||
'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False},
|
||||
'smlnj': {'id': 'SMLNJ', 'deprecated': False},
|
||||
'smppl': {'id': 'SMPPL', 'deprecated': False},
|
||||
'snia': {'id': 'SNIA', 'deprecated': False},
|
||||
'snprintf': {'id': 'snprintf', 'deprecated': False},
|
||||
'sofa': {'id': 'SOFA', 'deprecated': False},
|
||||
'softsurfer': {'id': 'softSurfer', 'deprecated': False},
|
||||
'soundex': {'id': 'Soundex', 'deprecated': False},
|
||||
'spencer-86': {'id': 'Spencer-86', 'deprecated': False},
|
||||
@@ -609,6 +636,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
|
||||
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
|
||||
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},
|
||||
'sul-1.0': {'id': 'SUL-1.0', 'deprecated': False},
|
||||
'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},
|
||||
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
|
||||
'sunpro': {'id': 'SunPro', 'deprecated': False},
|
||||
@@ -620,12 +648,14 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
|
||||
'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
|
||||
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
|
||||
'thirdeye': {'id': 'ThirdEye', 'deprecated': False},
|
||||
'threeparttable': {'id': 'threeparttable', 'deprecated': False},
|
||||
'tmate': {'id': 'TMate', 'deprecated': False},
|
||||
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
|
||||
'tosl': {'id': 'TOSL', 'deprecated': False},
|
||||
'tpdl': {'id': 'TPDL', 'deprecated': False},
|
||||
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
|
||||
'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False},
|
||||
'ttwl': {'id': 'TTWL', 'deprecated': False},
|
||||
'ttyp0': {'id': 'TTYP0', 'deprecated': False},
|
||||
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},
|
||||
@@ -641,6 +671,8 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
|
||||
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
|
||||
'unlicense': {'id': 'Unlicense', 'deprecated': False},
|
||||
'unlicense-libtelnet': {'id': 'Unlicense-libtelnet', 'deprecated': False},
|
||||
'unlicense-libwhirlpool': {'id': 'Unlicense-libwhirlpool', 'deprecated': False},
|
||||
'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
|
||||
'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
|
||||
'vim': {'id': 'Vim', 'deprecated': False},
|
||||
@@ -654,6 +686,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
||||
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
|
||||
'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
|
||||
'wtfpl': {'id': 'WTFPL', 'deprecated': False},
|
||||
'wwl': {'id': 'wwl', 'deprecated': False},
|
||||
'wxwindows': {'id': 'wxWindows', 'deprecated': True},
|
||||
'x11': {'id': 'X11', 'deprecated': False},
|
||||
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},
|
||||
@@ -695,9 +728,11 @@ EXCEPTIONS: dict[str, SPDXException] = {
|
||||
'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
|
||||
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
|
||||
'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},
|
||||
'cgal-linking-exception': {'id': 'CGAL-linking-exception', 'deprecated': False},
|
||||
'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
|
||||
'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},
|
||||
'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},
|
||||
'digia-qt-lgpl-exception-1.1': {'id': 'Digia-Qt-LGPL-exception-1.1', 'deprecated': False},
|
||||
'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
|
||||
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
|
||||
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},
|
||||
@@ -714,13 +749,16 @@ EXCEPTIONS: dict[str, SPDXException] = {
|
||||
'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
|
||||
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
|
||||
'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},
|
||||
'gpl-3.0-389-ds-base-exception': {'id': 'GPL-3.0-389-ds-base-exception', 'deprecated': False},
|
||||
'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},
|
||||
'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},
|
||||
'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},
|
||||
'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
|
||||
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
|
||||
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},
|
||||
'harbour-exception': {'id': 'harbour-exception', 'deprecated': False},
|
||||
'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},
|
||||
'independent-modules-exception': {'id': 'Independent-modules-exception', 'deprecated': False},
|
||||
'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
|
||||
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},
|
||||
'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
|
||||
@@ -730,12 +768,14 @@ EXCEPTIONS: dict[str, SPDXException] = {
|
||||
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
|
||||
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
|
||||
'mif-exception': {'id': 'mif-exception', 'deprecated': False},
|
||||
'mxml-exception': {'id': 'mxml-exception', 'deprecated': False},
|
||||
'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
|
||||
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
|
||||
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},
|
||||
'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
|
||||
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
|
||||
'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},
|
||||
'polyparse-exception': {'id': 'polyparse-exception', 'deprecated': False},
|
||||
'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},
|
||||
'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},
|
||||
'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
|
||||
|
||||
@@ -8,7 +8,7 @@ import operator
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from typing import AbstractSet, Any, Callable, Literal, TypedDict, Union, cast
|
||||
from typing import AbstractSet, Callable, Literal, Mapping, TypedDict, Union, cast
|
||||
|
||||
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
|
||||
from ._parser import parse_marker as _parse_marker
|
||||
@@ -17,6 +17,7 @@ from .specifiers import InvalidSpecifier, Specifier
|
||||
from .utils import canonicalize_name
|
||||
|
||||
__all__ = [
|
||||
"Environment",
|
||||
"EvaluateContext",
|
||||
"InvalidMarker",
|
||||
"Marker",
|
||||
@@ -25,31 +26,58 @@ __all__ = [
|
||||
"default_environment",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
|
||||
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
|
||||
"""A ``typing.Literal`` enumerating valid marker evaluation contexts.
|
||||
|
||||
Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
|
||||
|
||||
* ``"metadata"`` (for core metadata; default)
|
||||
* ``"lock_file"`` (for lock files)
|
||||
* ``"requirement"`` (i.e. all other situations)
|
||||
"""
|
||||
|
||||
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
|
||||
MARKERS_REQUIRING_VERSION = {
|
||||
"implementation_version",
|
||||
"platform_release",
|
||||
"python_full_version",
|
||||
"python_version",
|
||||
}
|
||||
|
||||
|
||||
class InvalidMarker(ValueError):
|
||||
"""
|
||||
An invalid marker was found, users should refer to PEP 508.
|
||||
"""Raised when attempting to create a :class:`Marker` from invalid input.
|
||||
|
||||
This error indicates that the given marker string does not conform to the
|
||||
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||
"""
|
||||
|
||||
|
||||
class UndefinedComparison(ValueError):
|
||||
"""
|
||||
An invalid operation was attempted on a value that doesn't support it.
|
||||
"""Raised when evaluating an unsupported marker comparison.
|
||||
|
||||
This can happen when marker values are compared as versions but do not
|
||||
conform to the :ref:`specification of version specifiers
|
||||
<pypug:version-specifiers>`.
|
||||
"""
|
||||
|
||||
|
||||
class UndefinedEnvironmentName(ValueError):
|
||||
"""
|
||||
A name was attempted to be used that does not exist inside of the
|
||||
environment.
|
||||
"""
|
||||
"""Raised when evaluating a marker that references a missing environment key."""
|
||||
|
||||
|
||||
class Environment(TypedDict):
|
||||
"""
|
||||
A dictionary that represents a Python environment as captured by
|
||||
:func:`default_environment`. All fields are required.
|
||||
"""
|
||||
|
||||
implementation_name: str
|
||||
"""The implementation's identifier, e.g. ``'cpython'``."""
|
||||
|
||||
@@ -121,20 +149,28 @@ class Environment(TypedDict):
|
||||
"""
|
||||
|
||||
|
||||
def _normalize_extra_values(results: Any) -> Any:
|
||||
def _normalize_extras(
|
||||
result: MarkerList | MarkerAtom | str,
|
||||
) -> MarkerList | MarkerAtom | str:
|
||||
if not isinstance(result, tuple):
|
||||
return result
|
||||
|
||||
lhs, op, rhs = result
|
||||
if isinstance(lhs, Variable) and lhs.value == "extra":
|
||||
normalized_extra = canonicalize_name(rhs.value)
|
||||
rhs = Value(normalized_extra)
|
||||
elif isinstance(rhs, Variable) and rhs.value == "extra":
|
||||
normalized_extra = canonicalize_name(lhs.value)
|
||||
lhs = Value(normalized_extra)
|
||||
return lhs, op, rhs
|
||||
|
||||
|
||||
def _normalize_extra_values(results: MarkerList) -> MarkerList:
|
||||
"""
|
||||
Normalize extra values.
|
||||
"""
|
||||
if isinstance(results[0], tuple):
|
||||
lhs, op, rhs = results[0]
|
||||
if isinstance(lhs, Variable) and lhs.value == "extra":
|
||||
normalized_extra = canonicalize_name(rhs.value)
|
||||
rhs = Value(normalized_extra)
|
||||
elif isinstance(rhs, Variable) and rhs.value == "extra":
|
||||
normalized_extra = canonicalize_name(lhs.value)
|
||||
lhs = Value(normalized_extra)
|
||||
results[0] = lhs, op, rhs
|
||||
return results
|
||||
|
||||
return [_normalize_extras(r) for r in results]
|
||||
|
||||
|
||||
def _format_marker(
|
||||
@@ -168,25 +204,26 @@ def _format_marker(
|
||||
_operators: dict[str, Operator] = {
|
||||
"in": lambda lhs, rhs: lhs in rhs,
|
||||
"not in": lambda lhs, rhs: lhs not in rhs,
|
||||
"<": operator.lt,
|
||||
"<=": operator.le,
|
||||
"<": lambda _lhs, _rhs: False,
|
||||
"<=": operator.eq,
|
||||
"==": operator.eq,
|
||||
"!=": operator.ne,
|
||||
">=": operator.ge,
|
||||
">": operator.gt,
|
||||
">=": operator.eq,
|
||||
">": lambda _lhs, _rhs: False,
|
||||
}
|
||||
|
||||
|
||||
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool:
|
||||
if isinstance(rhs, str):
|
||||
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool:
|
||||
op_str = op.serialize()
|
||||
if key in MARKERS_REQUIRING_VERSION:
|
||||
try:
|
||||
spec = Specifier("".join([op.serialize(), rhs]))
|
||||
spec = Specifier(f"{op_str}{rhs}")
|
||||
except InvalidSpecifier:
|
||||
pass
|
||||
else:
|
||||
return spec.contains(lhs, prereleases=True)
|
||||
|
||||
oper: Operator | None = _operators.get(op.serialize())
|
||||
oper: Operator | None = _operators.get(op_str)
|
||||
if oper is None:
|
||||
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
|
||||
|
||||
@@ -196,13 +233,14 @@ def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool:
|
||||
def _normalize(
|
||||
lhs: str, rhs: str | AbstractSet[str], key: str
|
||||
) -> tuple[str, str | AbstractSet[str]]:
|
||||
# PEP 685 – Comparison of extra names for optional distribution dependencies
|
||||
# PEP 685 - Comparison of extra names for optional distribution dependencies
|
||||
# https://peps.python.org/pep-0685/
|
||||
# > When comparing extra names, tools MUST normalize the names being
|
||||
# > compared using the semantics outlined in PEP 503 for names
|
||||
if key == "extra":
|
||||
assert isinstance(rhs, str), "extra value must be a string"
|
||||
return (canonicalize_name(lhs), canonicalize_name(rhs))
|
||||
# Both sides are normalized at this point already
|
||||
return (lhs, rhs)
|
||||
if key in MARKERS_ALLOWING_SET:
|
||||
if isinstance(rhs, str): # pragma: no cover
|
||||
return (canonicalize_name(lhs), canonicalize_name(rhs))
|
||||
@@ -219,8 +257,6 @@ def _evaluate_markers(
|
||||
groups: list[list[bool]] = [[]]
|
||||
|
||||
for marker in markers:
|
||||
assert isinstance(marker, (list, tuple, str))
|
||||
|
||||
if isinstance(marker, list):
|
||||
groups[-1].append(_evaluate_markers(marker, environment))
|
||||
elif isinstance(marker, tuple):
|
||||
@@ -234,18 +270,21 @@ def _evaluate_markers(
|
||||
lhs_value = lhs.value
|
||||
environment_key = rhs.value
|
||||
rhs_value = environment[environment_key]
|
||||
|
||||
assert isinstance(lhs_value, str), "lhs must be a string"
|
||||
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
|
||||
groups[-1].append(_eval_op(lhs_value, op, rhs_value))
|
||||
else:
|
||||
assert marker in ["and", "or"]
|
||||
if marker == "or":
|
||||
groups.append([])
|
||||
groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key))
|
||||
elif marker == "or":
|
||||
groups.append([])
|
||||
elif marker == "and":
|
||||
pass
|
||||
else: # pragma: nocover
|
||||
raise TypeError(f"Unexpected marker {marker!r}")
|
||||
|
||||
return any(all(item) for item in groups)
|
||||
|
||||
|
||||
def format_full_version(info: sys._version_info) -> str:
|
||||
def _format_full_version(info: sys._version_info) -> str:
|
||||
version = f"{info.major}.{info.minor}.{info.micro}"
|
||||
kind = info.releaselevel
|
||||
if kind != "final":
|
||||
@@ -254,7 +293,11 @@ def format_full_version(info: sys._version_info) -> str:
|
||||
|
||||
|
||||
def default_environment() -> Environment:
|
||||
iver = format_full_version(sys.implementation.version)
|
||||
"""Return the default marker environment for the current Python process.
|
||||
|
||||
This is the base environment used by :meth:`Marker.evaluate`.
|
||||
"""
|
||||
iver = _format_full_version(sys.implementation.version)
|
||||
implementation_name = sys.implementation.name
|
||||
return {
|
||||
"implementation_name": implementation_name,
|
||||
@@ -272,10 +315,36 @@ def default_environment() -> Environment:
|
||||
|
||||
|
||||
class Marker:
|
||||
"""Represents a parsed dependency marker expression.
|
||||
|
||||
Marker expressions are parsed according to the
|
||||
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||
|
||||
:param marker: The string representation of a marker expression.
|
||||
:raises InvalidMarker: If ``marker`` cannot be parsed.
|
||||
|
||||
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||
format so the same pickle can be loaded in future packaging releases.
|
||||
|
||||
.. versionchanged:: 26.2
|
||||
|
||||
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||
be unpickled with future releases. Backward compatibility with pickles
|
||||
from packaging < 26.2 is supported but may be removed in a future
|
||||
release.
|
||||
"""
|
||||
|
||||
__slots__ = ("_markers",)
|
||||
|
||||
def __init__(self, marker: str) -> None:
|
||||
# Note: We create a Marker object without calling this constructor in
|
||||
# packaging.requirements.Requirement. If any additional logic is
|
||||
# added here, make sure to mirror/adapt Requirement.
|
||||
|
||||
# If this fails and throws an error, the repr still expects _markers to
|
||||
# be defined.
|
||||
self._markers: MarkerList = []
|
||||
|
||||
try:
|
||||
self._markers = _normalize_extra_values(_parse_marker(marker))
|
||||
# The attribute `_markers` can be described in terms of a recursive type:
|
||||
@@ -297,36 +366,94 @@ class Marker:
|
||||
except ParserSyntaxError as e:
|
||||
raise InvalidMarker(str(e)) from e
|
||||
|
||||
@classmethod
|
||||
def _from_markers(cls, markers: MarkerList) -> Marker:
|
||||
"""Create a Marker instance from a pre-parsed marker tree.
|
||||
|
||||
This avoids re-parsing serialised marker strings when combining markers.
|
||||
"""
|
||||
new = cls.__new__(cls)
|
||||
new._markers = markers
|
||||
return new
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _format_marker(self._markers)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Marker('{self}')>"
|
||||
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.__class__.__name__, str(self)))
|
||||
return hash(str(self))
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Marker):
|
||||
return NotImplemented
|
||||
|
||||
return str(self) == str(other)
|
||||
|
||||
def __getstate__(self) -> str:
|
||||
# Return the marker expression string for compactness and stability.
|
||||
# Internal Node objects are excluded; the string is re-parsed on load.
|
||||
return str(self)
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, str):
|
||||
# New format (26.2+): just the marker expression string.
|
||||
try:
|
||||
self._markers = _normalize_extra_values(_parse_marker(state))
|
||||
except ParserSyntaxError as exc:
|
||||
raise TypeError(f"Cannot restore Marker from {state!r}") from exc
|
||||
return
|
||||
if isinstance(state, dict) and "_markers" in state:
|
||||
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
|
||||
markers = state["_markers"]
|
||||
if isinstance(markers, list):
|
||||
self._markers = markers
|
||||
return
|
||||
if isinstance(state, tuple) and len(state) == 2:
|
||||
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
|
||||
_, slot_dict = state
|
||||
if isinstance(slot_dict, dict) and "_markers" in slot_dict:
|
||||
markers = slot_dict["_markers"]
|
||||
if isinstance(markers, list):
|
||||
self._markers = markers
|
||||
return
|
||||
raise TypeError(f"Cannot restore Marker from {state!r}")
|
||||
|
||||
def __and__(self, other: Marker) -> Marker:
|
||||
if not isinstance(other, Marker):
|
||||
return NotImplemented
|
||||
return self._from_markers([self._markers, "and", other._markers])
|
||||
|
||||
def __or__(self, other: Marker) -> Marker:
|
||||
if not isinstance(other, Marker):
|
||||
return NotImplemented
|
||||
return self._from_markers([self._markers, "or", other._markers])
|
||||
|
||||
def evaluate(
|
||||
self,
|
||||
environment: dict[str, str] | None = None,
|
||||
environment: Mapping[str, str | AbstractSet[str]] | None = None,
|
||||
context: EvaluateContext = "metadata",
|
||||
) -> bool:
|
||||
"""Evaluate a marker.
|
||||
|
||||
Return the boolean from evaluating the given marker against the
|
||||
environment. environment is an optional argument to override all or
|
||||
part of the determined environment. The *context* parameter specifies what
|
||||
context the markers are being evaluated for, which influences what markers
|
||||
are considered valid. Acceptable values are "metadata" (for core metadata;
|
||||
default), "lock_file", and "requirement" (i.e. all other situations).
|
||||
Return the boolean from evaluating this marker against the environment.
|
||||
The environment is determined from the current Python process unless
|
||||
passed in explicitly.
|
||||
|
||||
:param environment: Mapping containing keys and values to override the
|
||||
detected environment.
|
||||
:param EvaluateContext context: The context in which the marker is
|
||||
evaluated, which influences what marker names are considered valid.
|
||||
Accepted values are ``"metadata"`` (for core metadata; default),
|
||||
``"lock_file"``, and ``"requirement"`` (i.e. all other situations).
|
||||
:raises UndefinedComparison: If the marker uses a comparison on values
|
||||
that are not valid versions per the :ref:`specification of version
|
||||
specifiers <pypug:version-specifiers>`.
|
||||
:raises UndefinedEnvironmentName: If the marker references a value that
|
||||
is missing from the evaluation environment.
|
||||
:returns: ``True`` if the marker matches, otherwise ``False``.
|
||||
|
||||
The environment is determined from the current Python process.
|
||||
"""
|
||||
current_environment = cast(
|
||||
"dict[str, str | AbstractSet[str]]", default_environment()
|
||||
@@ -337,12 +464,15 @@ class Marker:
|
||||
)
|
||||
elif context == "metadata":
|
||||
current_environment["extra"] = ""
|
||||
|
||||
if environment is not None:
|
||||
current_environment.update(environment)
|
||||
# The API used to allow setting extra to None. We need to handle this
|
||||
# case for backwards compatibility.
|
||||
if "extra" in current_environment and current_environment["extra"] is None:
|
||||
current_environment["extra"] = ""
|
||||
if "extra" in current_environment:
|
||||
# The API used to allow setting extra to None. We need to handle
|
||||
# this case for backwards compatibility. Also skip running
|
||||
# normalize name if extra is empty.
|
||||
extra = cast("str | None", current_environment["extra"])
|
||||
current_environment["extra"] = canonicalize_name(extra) if extra else ""
|
||||
|
||||
return _evaluate_markers(
|
||||
self._markers, _repair_python_full_version(current_environment)
|
||||
@@ -356,7 +486,7 @@ def _repair_python_full_version(
|
||||
Work around platform.python_version() returning something that is not PEP 440
|
||||
compliant for non-tagged Python builds.
|
||||
"""
|
||||
python_full_version = cast(str, env["python_full_version"])
|
||||
python_full_version = cast("str", env["python_full_version"])
|
||||
if python_full_version.endswith("+"):
|
||||
env["python_full_version"] = f"{python_full_version}local"
|
||||
return env
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import email.feedparser
|
||||
import email.header
|
||||
import email.message
|
||||
import email.parser
|
||||
import email.policy
|
||||
import keyword
|
||||
import pathlib
|
||||
import sys
|
||||
import typing
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -19,31 +18,27 @@ from typing import (
|
||||
|
||||
from . import licenses, requirements, specifiers, utils
|
||||
from . import version as version_module
|
||||
from .licenses import NormalizedLicenseExpression
|
||||
from .errors import ExceptionGroup, _ErrorCollector
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .licenses import NormalizedLicenseExpression
|
||||
|
||||
T = typing.TypeVar("T")
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11): # pragma: no cover
|
||||
ExceptionGroup = ExceptionGroup
|
||||
else: # pragma: no cover
|
||||
__all__ = [
|
||||
"ExceptionGroup", # Keep this for a bit (makes mypy happy w/ 26.0 compat)
|
||||
"InvalidMetadata",
|
||||
"Metadata",
|
||||
"RFC822Message",
|
||||
"RFC822Policy",
|
||||
"RawMetadata",
|
||||
"parse_email",
|
||||
]
|
||||
|
||||
class ExceptionGroup(Exception):
|
||||
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
|
||||
|
||||
If :external:exc:`ExceptionGroup` is already defined by Python itself,
|
||||
that version is used instead.
|
||||
"""
|
||||
|
||||
message: str
|
||||
exceptions: list[Exception]
|
||||
|
||||
def __init__(self, message: str, exceptions: list[Exception]) -> None:
|
||||
self.message = message
|
||||
self.exceptions = exceptions
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
class InvalidMetadata(ValueError):
|
||||
@@ -73,8 +68,8 @@ class RawMetadata(TypedDict, total=False):
|
||||
|
||||
Core metadata fields that can be specified multiple times are stored as a
|
||||
list or dict depending on which is appropriate for the field. Any fields
|
||||
which hold multiple values in a single field are stored as a list.
|
||||
|
||||
which hold multiple values in a single field are stored as a list. All fields
|
||||
are considered optional.
|
||||
"""
|
||||
|
||||
# Metadata 1.0 - PEP 241
|
||||
@@ -126,13 +121,19 @@ class RawMetadata(TypedDict, total=False):
|
||||
|
||||
# Metadata 2.3 - PEP 685
|
||||
# No new fields were added in PEP 685, just some edge case were
|
||||
# tightened up to provide better interoptability.
|
||||
# tightened up to provide better interoperability.
|
||||
|
||||
# Metadata 2.4 - PEP 639
|
||||
license_expression: str
|
||||
license_files: list[str]
|
||||
|
||||
# Metadata 2.5 - PEP 794
|
||||
import_names: list[str]
|
||||
import_namespaces: list[str]
|
||||
|
||||
|
||||
# 'keywords' is special as it's a string in the core metadata spec, but we
|
||||
# represent it as a list.
|
||||
_STRING_FIELDS = {
|
||||
"author",
|
||||
"author_email",
|
||||
@@ -165,6 +166,8 @@ _LIST_FIELDS = {
|
||||
"requires_dist",
|
||||
"requires_external",
|
||||
"supported_platforms",
|
||||
"import_names",
|
||||
"import_namespaces",
|
||||
}
|
||||
|
||||
_DICT_FIELDS = {
|
||||
@@ -193,24 +196,23 @@ def _parse_project_urls(data: list[str]) -> dict[str, str]:
|
||||
# be the missing value, then they'd have multiple '' values that
|
||||
# overwrite each other in a accumulating dict.
|
||||
#
|
||||
# The other potentional issue is that it's possible to have the
|
||||
# The other potential issue is that it's possible to have the
|
||||
# same label multiple times in the metadata, with no solid "right"
|
||||
# answer with what to do in that case. As such, we'll do the only
|
||||
# thing we can, which is treat the field as unparseable and add it
|
||||
# thing we can, which is treat the field as unparsable and add it
|
||||
# to our list of unparsed fields.
|
||||
parts = [p.strip() for p in pair.split(",", 1)]
|
||||
parts.extend([""] * (max(0, 2 - len(parts)))) # Ensure 2 items
|
||||
|
||||
#
|
||||
# TODO: The spec doesn't say anything about if the keys should be
|
||||
# considered case sensitive or not... logically they should
|
||||
# be case-preserving and case-insensitive, but doing that
|
||||
# would open up more cases where we might have duplicate
|
||||
# entries.
|
||||
label, url = parts
|
||||
label, _, url = (s.strip() for s in pair.partition(","))
|
||||
|
||||
if label in urls:
|
||||
# The label already exists in our set of urls, so this field
|
||||
# is unparseable, and we can just add the whole thing to our
|
||||
# unparseable data and stop processing it.
|
||||
# is unparsable, and we can just add the whole thing to our
|
||||
# unparsable data and stop processing it.
|
||||
raise KeyError("duplicate labels in project urls")
|
||||
urls[label] = url
|
||||
|
||||
@@ -257,6 +259,8 @@ _EMAIL_TO_RAW_MAPPING = {
|
||||
"download-url": "download_url",
|
||||
"dynamic": "dynamic",
|
||||
"home-page": "home_page",
|
||||
"import-name": "import_names",
|
||||
"import-namespace": "import_namespaces",
|
||||
"keywords": "keywords",
|
||||
"license": "license",
|
||||
"license-expression": "license_expression",
|
||||
@@ -283,6 +287,45 @@ _EMAIL_TO_RAW_MAPPING = {
|
||||
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
|
||||
|
||||
|
||||
# This class is for writing RFC822 messages
|
||||
class RFC822Policy(email.policy.EmailPolicy):
|
||||
"""
|
||||
This is :class:`email.policy.EmailPolicy`, but with a simple ``header_store_parse``
|
||||
implementation that handles multi-line values, and some nice defaults.
|
||||
"""
|
||||
|
||||
utf8 = True
|
||||
mangle_from_ = False
|
||||
max_line_length = 0
|
||||
|
||||
def header_store_parse(self, name: str, value: str) -> tuple[str, str]:
|
||||
size = len(name) + 2
|
||||
value = value.replace("\n", "\n" + " " * size)
|
||||
return (name, value)
|
||||
|
||||
|
||||
# This class is for writing RFC822 messages
|
||||
class RFC822Message(email.message.EmailMessage):
|
||||
"""
|
||||
This is :class:`email.message.EmailMessage` with two small changes: it defaults to
|
||||
our `RFC822Policy`, and it correctly writes unicode when being called
|
||||
with `bytes()`.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(policy=RFC822Policy())
|
||||
|
||||
def as_bytes(
|
||||
self, unixfrom: bool = False, policy: email.policy.Policy | None = None
|
||||
) -> bytes:
|
||||
"""
|
||||
Return the bytes representation of the message.
|
||||
|
||||
This handles unicode encoding.
|
||||
"""
|
||||
return self.as_string(unixfrom, policy=policy).encode("utf-8")
|
||||
|
||||
|
||||
def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
||||
"""Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``).
|
||||
|
||||
@@ -310,10 +353,10 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
||||
# We have to wrap parsed.keys() in a set, because in the case of multiple
|
||||
# values for a key (a list), the key will appear multiple times in the
|
||||
# list of keys, but we're avoiding that by using get_all().
|
||||
for name in frozenset(parsed.keys()):
|
||||
for name_with_case in frozenset(parsed.keys()):
|
||||
# Header names in RFC are case insensitive, so we'll normalize to all
|
||||
# lower case to make comparisons easier.
|
||||
name = name.lower()
|
||||
name = name_with_case.lower()
|
||||
|
||||
# We use get_all() here, even for fields that aren't multiple use,
|
||||
# because otherwise someone could have e.g. two Name fields, and we
|
||||
@@ -349,16 +392,16 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
||||
# can be independently encoded, so we'll need to check each
|
||||
# of them.
|
||||
chunks: list[tuple[bytes, str | None]] = []
|
||||
for bin, encoding in email.header.decode_header(h):
|
||||
for binary, _encoding in email.header.decode_header(h):
|
||||
try:
|
||||
bin.decode("utf8", "strict")
|
||||
binary.decode("utf8", "strict")
|
||||
except UnicodeDecodeError:
|
||||
# Enable mojibake.
|
||||
encoding = "latin1"
|
||||
valid_encoding = False
|
||||
else:
|
||||
encoding = "utf8"
|
||||
chunks.append((bin, encoding))
|
||||
chunks.append((binary, encoding))
|
||||
|
||||
# Turn our chunks back into a Header object, then let that
|
||||
# Header object do the right thing to turn them into a
|
||||
@@ -397,6 +440,11 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
||||
# of unparsed stuff.
|
||||
if raw_name in _STRING_FIELDS and len(value) == 1:
|
||||
raw[raw_name] = value[0]
|
||||
# If this is import_names, we need to special case the empty field
|
||||
# case, which converts to an empty list instead of None. We can't let
|
||||
# the empty case slip through, as it will fail validation.
|
||||
elif raw_name == "import_names" and value == [""]:
|
||||
raw[raw_name] = []
|
||||
# If this is one of our list of string fields, then we can just assign
|
||||
# the value, since email *only* has strings, and our get_all() call
|
||||
# above ensures that this is a list.
|
||||
@@ -424,7 +472,7 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
||||
except KeyError:
|
||||
unparsed[name] = value
|
||||
# Nothing that we've done has managed to parse this, so it'll just
|
||||
# throw it in our unparseable data and move on.
|
||||
# throw it in our unparsable data and move on.
|
||||
else:
|
||||
unparsed[name] = value
|
||||
|
||||
@@ -441,9 +489,9 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
||||
else:
|
||||
if payload:
|
||||
# Check to see if we've already got a description, if so then both
|
||||
# it, and this body move to unparseable.
|
||||
# it, and this body move to unparsable.
|
||||
if "description" in raw:
|
||||
description_header = cast(str, raw.pop("description"))
|
||||
description_header = cast("str", raw.pop("description"))
|
||||
unparsed.setdefault("description", []).extend(
|
||||
[description_header, payload]
|
||||
)
|
||||
@@ -456,15 +504,15 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
||||
# literal key names, but we're computing our key names on purpose, but the
|
||||
# way this function is implemented, our `TypedDict` can only have valid key
|
||||
# names.
|
||||
return cast(RawMetadata, raw), unparsed
|
||||
return cast("RawMetadata", raw), unparsed
|
||||
|
||||
|
||||
_NOT_FOUND = object()
|
||||
|
||||
|
||||
# Keep the two values in sync.
|
||||
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
|
||||
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
|
||||
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
|
||||
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
|
||||
|
||||
_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
|
||||
|
||||
@@ -519,7 +567,7 @@ class _Validator(Generic[T]):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return cast(T, value)
|
||||
return cast("T", value)
|
||||
|
||||
def _invalid_metadata(
|
||||
self, msg: str, cause: Exception | None = None
|
||||
@@ -534,7 +582,7 @@ class _Validator(Generic[T]):
|
||||
# Implicitly makes Metadata-Version required.
|
||||
if value not in _VALID_METADATA_VERSIONS:
|
||||
raise self._invalid_metadata(f"{value!r} is not a valid metadata version")
|
||||
return cast(_MetadataVersion, value)
|
||||
return cast("_MetadataVersion", value)
|
||||
|
||||
def _process_name(self, value: str) -> str:
|
||||
if not value:
|
||||
@@ -585,7 +633,7 @@ class _Validator(Generic[T]):
|
||||
charset = parameters.get("charset", "UTF-8")
|
||||
if charset != "UTF-8":
|
||||
raise self._invalid_metadata(
|
||||
f"{{field}} can only specify the UTF-8 charset, not {list(charset)}"
|
||||
f"{{field}} can only specify the UTF-8 charset, not {charset!r}"
|
||||
)
|
||||
|
||||
markdown_variants = {"GFM", "CommonMark"}
|
||||
@@ -647,9 +695,7 @@ class _Validator(Generic[T]):
|
||||
else:
|
||||
return reqs
|
||||
|
||||
def _process_license_expression(
|
||||
self, value: str
|
||||
) -> NormalizedLicenseExpression | None:
|
||||
def _process_license_expression(self, value: str) -> NormalizedLicenseExpression:
|
||||
try:
|
||||
return licenses.canonicalize_license_expression(value)
|
||||
except ValueError as exc:
|
||||
@@ -683,6 +729,30 @@ class _Validator(Generic[T]):
|
||||
paths.append(path)
|
||||
return paths
|
||||
|
||||
def _process_import_names(self, value: list[str]) -> list[str]:
|
||||
for import_name in value:
|
||||
name, semicolon, private = import_name.partition(";")
|
||||
name = name.rstrip()
|
||||
for identifier in name.split("."):
|
||||
if not identifier.isidentifier():
|
||||
raise self._invalid_metadata(
|
||||
f"{name!r} is invalid for {{field}}; "
|
||||
f"{identifier!r} is not a valid identifier"
|
||||
)
|
||||
elif keyword.iskeyword(identifier):
|
||||
raise self._invalid_metadata(
|
||||
f"{name!r} is invalid for {{field}}; "
|
||||
f"{identifier!r} is a keyword"
|
||||
)
|
||||
if semicolon and private.lstrip() != "private":
|
||||
raise self._invalid_metadata(
|
||||
f"{import_name!r} is invalid for {{field}}; "
|
||||
"the only valid option is 'private'"
|
||||
)
|
||||
return value
|
||||
|
||||
_process_import_namespaces = _process_import_names
|
||||
|
||||
|
||||
class Metadata:
|
||||
"""Representation of distribution metadata.
|
||||
@@ -706,13 +776,11 @@ class Metadata:
|
||||
ins._raw = data.copy() # Mutations occur due to caching enriched values.
|
||||
|
||||
if validate:
|
||||
exceptions: list[Exception] = []
|
||||
try:
|
||||
collector = _ErrorCollector()
|
||||
metadata_version = None
|
||||
with collector.collect(InvalidMetadata):
|
||||
metadata_version = ins.metadata_version
|
||||
metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version)
|
||||
except InvalidMetadata as metadata_version_exc:
|
||||
exceptions.append(metadata_version_exc)
|
||||
metadata_version = None
|
||||
|
||||
# Make sure to check for the fields that are present, the required
|
||||
# fields (so their absence can be reported).
|
||||
@@ -729,7 +797,7 @@ class Metadata:
|
||||
field_metadata_version = cls.__dict__[key].added
|
||||
except KeyError:
|
||||
exc = InvalidMetadata(key, f"unrecognized field: {key!r}")
|
||||
exceptions.append(exc)
|
||||
collector.error(exc)
|
||||
continue
|
||||
field_age = _VALID_METADATA_VERSIONS.index(
|
||||
field_metadata_version
|
||||
@@ -741,14 +809,13 @@ class Metadata:
|
||||
f"{field} introduced in metadata version "
|
||||
f"{field_metadata_version}, not {metadata_version}",
|
||||
)
|
||||
exceptions.append(exc)
|
||||
collector.error(exc)
|
||||
continue
|
||||
getattr(ins, key)
|
||||
except InvalidMetadata as exc:
|
||||
exceptions.append(exc)
|
||||
collector.error(exc)
|
||||
|
||||
if exceptions:
|
||||
raise ExceptionGroup("invalid metadata", exceptions)
|
||||
collector.finalize("invalid metadata")
|
||||
|
||||
return ins
|
||||
|
||||
@@ -762,16 +829,13 @@ class Metadata:
|
||||
raw, unparsed = parse_email(data)
|
||||
|
||||
if validate:
|
||||
exceptions: list[Exception] = []
|
||||
for unparsed_key in unparsed:
|
||||
if unparsed_key in _EMAIL_TO_RAW_MAPPING:
|
||||
message = f"{unparsed_key!r} has invalid data"
|
||||
else:
|
||||
message = f"unrecognized field: {unparsed_key!r}"
|
||||
exceptions.append(InvalidMetadata(unparsed_key, message))
|
||||
|
||||
if exceptions:
|
||||
raise ExceptionGroup("unparsed", exceptions)
|
||||
with _ErrorCollector().on_exit("unparsed") as collector:
|
||||
for unparsed_key in unparsed:
|
||||
if unparsed_key in _EMAIL_TO_RAW_MAPPING:
|
||||
message = f"{unparsed_key!r} has invalid data"
|
||||
else:
|
||||
message = f"unrecognized field: {unparsed_key!r}"
|
||||
collector.error(InvalidMetadata(unparsed_key, message))
|
||||
|
||||
try:
|
||||
return cls.from_raw(raw, validate=validate)
|
||||
@@ -854,9 +918,47 @@ class Metadata:
|
||||
""":external:ref:`core-metadata-provides-dist`"""
|
||||
obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
|
||||
""":external:ref:`core-metadata-obsoletes-dist`"""
|
||||
import_names: _Validator[list[str] | None] = _Validator(added="2.5")
|
||||
""":external:ref:`core-metadata-import-name`"""
|
||||
import_namespaces: _Validator[list[str] | None] = _Validator(added="2.5")
|
||||
""":external:ref:`core-metadata-import-namespace`"""
|
||||
requires: _Validator[list[str] | None] = _Validator(added="1.1")
|
||||
"""``Requires`` (deprecated)"""
|
||||
provides: _Validator[list[str] | None] = _Validator(added="1.1")
|
||||
"""``Provides`` (deprecated)"""
|
||||
obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
|
||||
"""``Obsoletes`` (deprecated)"""
|
||||
|
||||
def as_rfc822(self) -> RFC822Message:
|
||||
"""
|
||||
Return an RFC822 message with the metadata.
|
||||
"""
|
||||
message = RFC822Message()
|
||||
self._write_metadata(message)
|
||||
return message
|
||||
|
||||
def _write_metadata(self, message: RFC822Message) -> None:
|
||||
"""
|
||||
Return an RFC822 message with the metadata.
|
||||
"""
|
||||
for name, validator in self.__class__.__dict__.items():
|
||||
if isinstance(validator, _Validator) and name != "description":
|
||||
value = getattr(self, name)
|
||||
email_name = _RAW_TO_EMAIL_MAPPING[name]
|
||||
if value is not None:
|
||||
if email_name == "project-url":
|
||||
for label, url in value.items():
|
||||
message[email_name] = f"{label}, {url}"
|
||||
elif email_name == "keywords":
|
||||
message[email_name] = ",".join(value)
|
||||
elif email_name == "import-name" and value == []:
|
||||
message[email_name] = ""
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
message[email_name] = str(item)
|
||||
else:
|
||||
message[email_name] = str(value)
|
||||
|
||||
# The description is a special case because it is in the body of the message.
|
||||
if self.description is not None:
|
||||
message.set_payload(self.description)
|
||||
|
||||
@@ -0,0 +1,905 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .markers import Environment, Marker, default_environment
|
||||
from .specifiers import SpecifierSet
|
||||
from .tags import create_compatible_tags_selector, sys_tags
|
||||
from .utils import (
|
||||
NormalizedName,
|
||||
is_normalized_name,
|
||||
parse_sdist_filename,
|
||||
parse_wheel_filename,
|
||||
)
|
||||
from .version import Version
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from collections.abc import Collection, Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from .tags import Tag
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"Package",
|
||||
"PackageArchive",
|
||||
"PackageDirectory",
|
||||
"PackageSdist",
|
||||
"PackageVcs",
|
||||
"PackageWheel",
|
||||
"Pylock",
|
||||
"PylockUnsupportedVersionError",
|
||||
"PylockValidationError",
|
||||
"is_valid_pylock_path",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_T2 = TypeVar("_T2")
|
||||
|
||||
|
||||
class _FromMappingProtocol(Protocol): # pragma: no cover
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
|
||||
|
||||
|
||||
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
|
||||
|
||||
|
||||
_PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
|
||||
|
||||
|
||||
def is_valid_pylock_path(path: Path) -> bool:
|
||||
"""Check if the given path is a valid pylock file path."""
|
||||
return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name))
|
||||
|
||||
|
||||
def _toml_key(key: str) -> str:
|
||||
return key.replace("_", "-")
|
||||
|
||||
|
||||
def _toml_value(key: str, value: Any) -> Any: # noqa: ANN401
|
||||
if isinstance(value, (Version, Marker, SpecifierSet)):
|
||||
return str(value)
|
||||
if isinstance(value, Sequence) and key == "environments":
|
||||
return [str(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||
return {
|
||||
_toml_key(key): _toml_value(key, value)
|
||||
for key, value in data
|
||||
if value is not None
|
||||
}
|
||||
|
||||
|
||||
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
|
||||
"""Get a value from the dictionary and verify it's the expected type."""
|
||||
if (value := d.get(key)) is None:
|
||||
return None
|
||||
if not isinstance(value, expected_type):
|
||||
raise PylockValidationError(
|
||||
f"Unexpected type {type(value).__name__} "
|
||||
f"(expected {expected_type.__name__})",
|
||||
context=key,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
|
||||
"""Get a required value from the dictionary and verify it's the expected type."""
|
||||
if (value := _get(d, expected_type, key)) is None:
|
||||
raise _PylockRequiredKeyError(key)
|
||||
return value
|
||||
|
||||
|
||||
def _get_sequence(
|
||||
d: Mapping[str, Any], expected_item_type: type[_T], key: str
|
||||
) -> Sequence[_T] | None:
|
||||
"""Get a list value from the dictionary and verify it's the expected items type."""
|
||||
if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract]
|
||||
return None
|
||||
if isinstance(value, (str, bytes)):
|
||||
# special case: str and bytes are Sequences, but we want to reject it
|
||||
raise PylockValidationError(
|
||||
f"Unexpected type {type(value).__name__} (expected Sequence)",
|
||||
context=key,
|
||||
)
|
||||
for i, item in enumerate(value):
|
||||
if not isinstance(item, expected_item_type):
|
||||
raise PylockValidationError(
|
||||
f"Unexpected type {type(item).__name__} "
|
||||
f"(expected {expected_item_type.__name__})",
|
||||
context=f"{key}[{i}]",
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _get_as(
|
||||
d: Mapping[str, Any],
|
||||
expected_type: type[_T],
|
||||
target_type: Callable[[_T], _T2],
|
||||
key: str,
|
||||
) -> _T2 | None:
|
||||
"""Get a value from the dictionary, verify it's the expected type,
|
||||
and convert to the target type.
|
||||
|
||||
This assumes the target_type constructor accepts the value.
|
||||
"""
|
||||
if (value := _get(d, expected_type, key)) is None:
|
||||
return None
|
||||
try:
|
||||
return target_type(value)
|
||||
except Exception as e:
|
||||
raise PylockValidationError(e, context=key) from e
|
||||
|
||||
|
||||
def _get_required_as(
|
||||
d: Mapping[str, Any],
|
||||
expected_type: type[_T],
|
||||
target_type: Callable[[_T], _T2],
|
||||
key: str,
|
||||
) -> _T2:
|
||||
"""Get a required value from the dict, verify it's the expected type,
|
||||
and convert to the target type."""
|
||||
if (value := _get_as(d, expected_type, target_type, key)) is None:
|
||||
raise _PylockRequiredKeyError(key)
|
||||
return value
|
||||
|
||||
|
||||
def _get_sequence_as(
|
||||
d: Mapping[str, Any],
|
||||
expected_item_type: type[_T],
|
||||
target_item_type: Callable[[_T], _T2],
|
||||
key: str,
|
||||
) -> list[_T2] | None:
|
||||
"""Get list value from dictionary and verify expected items type."""
|
||||
if (value := _get_sequence(d, expected_item_type, key)) is None:
|
||||
return None
|
||||
result = []
|
||||
try:
|
||||
for item in value:
|
||||
typed_item = target_item_type(item)
|
||||
result.append(typed_item)
|
||||
except Exception as e:
|
||||
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
|
||||
return result
|
||||
|
||||
|
||||
def _get_object(
|
||||
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
|
||||
) -> _FromMappingProtocolT | None:
|
||||
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
|
||||
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
|
||||
return None
|
||||
try:
|
||||
return target_type._from_dict(value)
|
||||
except Exception as e:
|
||||
raise PylockValidationError(e, context=key) from e
|
||||
|
||||
|
||||
def _get_sequence_of_objects(
|
||||
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
|
||||
) -> list[_FromMappingProtocolT] | None:
|
||||
"""Get a list value from the dictionary and convert its items to a dataclass."""
|
||||
if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract]
|
||||
return None
|
||||
result: list[_FromMappingProtocolT] = []
|
||||
try:
|
||||
for item in value:
|
||||
typed_item = target_item_type._from_dict(item)
|
||||
result.append(typed_item)
|
||||
except Exception as e:
|
||||
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
|
||||
return result
|
||||
|
||||
|
||||
def _get_required_sequence_of_objects(
|
||||
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
|
||||
) -> Sequence[_FromMappingProtocolT]:
|
||||
"""Get a required list value from the dictionary and convert its items to a
|
||||
dataclass."""
|
||||
if (result := _get_sequence_of_objects(d, target_item_type, key)) is None:
|
||||
raise _PylockRequiredKeyError(key)
|
||||
return result
|
||||
|
||||
|
||||
def _validate_normalized_name(name: str) -> NormalizedName:
|
||||
"""Validate that a string is a NormalizedName."""
|
||||
if not is_normalized_name(name):
|
||||
raise PylockValidationError(f"Name {name!r} is not normalized")
|
||||
return NormalizedName(name)
|
||||
|
||||
|
||||
def _validate_path_url(path: str | None, url: str | None) -> None:
|
||||
if not path and not url:
|
||||
raise PylockValidationError("path or url must be provided")
|
||||
|
||||
|
||||
def _path_name(path: str | None) -> str | None:
|
||||
if not path:
|
||||
return None
|
||||
# If the path is relative it MAY use POSIX-style path separators explicitly
|
||||
# for portability
|
||||
if "/" in path:
|
||||
return path.rsplit("/", 1)[-1]
|
||||
elif "\\" in path:
|
||||
return path.rsplit("\\", 1)[-1]
|
||||
else:
|
||||
return path
|
||||
|
||||
|
||||
def _url_name(url: str | None) -> str | None:
|
||||
if not url:
|
||||
return None
|
||||
url_path = urlparse(url).path
|
||||
return url_path.rsplit("/", 1)[-1]
|
||||
|
||||
|
||||
def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if not hashes:
|
||||
raise PylockValidationError("At least one hash must be provided")
|
||||
if not all(isinstance(hash_val, str) for hash_val in hashes.values()):
|
||||
raise PylockValidationError("Hash values must be strings")
|
||||
return hashes
|
||||
|
||||
|
||||
class PylockValidationError(Exception):
|
||||
"""Raised when when input data is not spec-compliant."""
|
||||
|
||||
context: str | None = None
|
||||
message: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cause: str | Exception,
|
||||
*,
|
||||
context: str | None = None,
|
||||
) -> None:
|
||||
if isinstance(cause, PylockValidationError):
|
||||
if cause.context:
|
||||
self.context = (
|
||||
f"{context}.{cause.context}" if context else cause.context
|
||||
)
|
||||
else:
|
||||
self.context = context
|
||||
self.message = cause.message
|
||||
else:
|
||||
self.context = context
|
||||
self.message = str(cause)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.context:
|
||||
return f"{self.message} in {self.context!r}"
|
||||
return self.message
|
||||
|
||||
|
||||
class _PylockRequiredKeyError(PylockValidationError):
|
||||
def __init__(self, key: str) -> None:
|
||||
super().__init__("Missing required value", context=key)
|
||||
|
||||
|
||||
class PylockUnsupportedVersionError(PylockValidationError):
|
||||
"""Raised when encountering an unsupported `lock_version`."""
|
||||
|
||||
|
||||
class PylockSelectError(Exception):
|
||||
"""Base exception for errors raised by :meth:`Pylock.select`."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class PackageVcs:
|
||||
type: str
|
||||
url: str | None = None
|
||||
path: str | None = None
|
||||
requested_revision: str | None = None
|
||||
commit_id: str # type: ignore[misc]
|
||||
subdirectory: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
type: str,
|
||||
url: str | None = None,
|
||||
path: str | None = None,
|
||||
requested_revision: str | None = None,
|
||||
commit_id: str,
|
||||
subdirectory: str | None = None,
|
||||
) -> None:
|
||||
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||
object.__setattr__(self, "type", type)
|
||||
object.__setattr__(self, "url", url)
|
||||
object.__setattr__(self, "path", path)
|
||||
object.__setattr__(self, "requested_revision", requested_revision)
|
||||
object.__setattr__(self, "commit_id", commit_id)
|
||||
object.__setattr__(self, "subdirectory", subdirectory)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
package_vcs = cls(
|
||||
type=_get_required(d, str, "type"),
|
||||
url=_get(d, str, "url"),
|
||||
path=_get(d, str, "path"),
|
||||
requested_revision=_get(d, str, "requested-revision"),
|
||||
commit_id=_get_required(d, str, "commit-id"),
|
||||
subdirectory=_get(d, str, "subdirectory"),
|
||||
)
|
||||
_validate_path_url(package_vcs.path, package_vcs.url)
|
||||
return package_vcs
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class PackageDirectory:
|
||||
path: str
|
||||
editable: bool | None = None
|
||||
subdirectory: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
path: str,
|
||||
editable: bool | None = None,
|
||||
subdirectory: str | None = None,
|
||||
) -> None:
|
||||
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||
object.__setattr__(self, "path", path)
|
||||
object.__setattr__(self, "editable", editable)
|
||||
object.__setattr__(self, "subdirectory", subdirectory)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
return cls(
|
||||
path=_get_required(d, str, "path"),
|
||||
editable=_get(d, bool, "editable"),
|
||||
subdirectory=_get(d, str, "subdirectory"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class PackageArchive:
|
||||
url: str | None = None
|
||||
path: str | None = None
|
||||
size: int | None = None
|
||||
upload_time: datetime | None = None
|
||||
hashes: Mapping[str, str] # type: ignore[misc]
|
||||
subdirectory: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
url: str | None = None,
|
||||
path: str | None = None,
|
||||
size: int | None = None,
|
||||
upload_time: datetime | None = None,
|
||||
hashes: Mapping[str, str],
|
||||
subdirectory: str | None = None,
|
||||
) -> None:
|
||||
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||
object.__setattr__(self, "url", url)
|
||||
object.__setattr__(self, "path", path)
|
||||
object.__setattr__(self, "size", size)
|
||||
object.__setattr__(self, "upload_time", upload_time)
|
||||
object.__setattr__(self, "hashes", hashes)
|
||||
object.__setattr__(self, "subdirectory", subdirectory)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
package_archive = cls(
|
||||
url=_get(d, str, "url"),
|
||||
path=_get(d, str, "path"),
|
||||
size=_get(d, int, "size"),
|
||||
upload_time=_get(d, datetime, "upload-time"),
|
||||
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
|
||||
subdirectory=_get(d, str, "subdirectory"),
|
||||
)
|
||||
_validate_path_url(package_archive.path, package_archive.url)
|
||||
return package_archive
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class PackageSdist:
|
||||
name: str | None = None
|
||||
upload_time: datetime | None = None
|
||||
url: str | None = None
|
||||
path: str | None = None
|
||||
size: int | None = None
|
||||
hashes: Mapping[str, str] # type: ignore[misc]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str | None = None,
|
||||
upload_time: datetime | None = None,
|
||||
url: str | None = None,
|
||||
path: str | None = None,
|
||||
size: int | None = None,
|
||||
hashes: Mapping[str, str],
|
||||
) -> None:
|
||||
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||
object.__setattr__(self, "name", name)
|
||||
object.__setattr__(self, "upload_time", upload_time)
|
||||
object.__setattr__(self, "url", url)
|
||||
object.__setattr__(self, "path", path)
|
||||
object.__setattr__(self, "size", size)
|
||||
object.__setattr__(self, "hashes", hashes)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
package_sdist = cls(
|
||||
name=_get(d, str, "name"),
|
||||
upload_time=_get(d, datetime, "upload-time"),
|
||||
url=_get(d, str, "url"),
|
||||
path=_get(d, str, "path"),
|
||||
size=_get(d, int, "size"),
|
||||
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
|
||||
)
|
||||
_validate_path_url(package_sdist.path, package_sdist.url)
|
||||
return package_sdist
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
"""Get the filename of the sdist."""
|
||||
filename = self.name or _path_name(self.path) or _url_name(self.url)
|
||||
if not filename:
|
||||
raise PylockValidationError("Cannot determine sdist filename")
|
||||
return filename
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class PackageWheel:
|
||||
name: str | None = None
|
||||
upload_time: datetime | None = None
|
||||
url: str | None = None
|
||||
path: str | None = None
|
||||
size: int | None = None
|
||||
hashes: Mapping[str, str] # type: ignore[misc]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str | None = None,
|
||||
upload_time: datetime | None = None,
|
||||
url: str | None = None,
|
||||
path: str | None = None,
|
||||
size: int | None = None,
|
||||
hashes: Mapping[str, str],
|
||||
) -> None:
|
||||
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||
object.__setattr__(self, "name", name)
|
||||
object.__setattr__(self, "upload_time", upload_time)
|
||||
object.__setattr__(self, "url", url)
|
||||
object.__setattr__(self, "path", path)
|
||||
object.__setattr__(self, "size", size)
|
||||
object.__setattr__(self, "hashes", hashes)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
package_wheel = cls(
|
||||
name=_get(d, str, "name"),
|
||||
upload_time=_get(d, datetime, "upload-time"),
|
||||
url=_get(d, str, "url"),
|
||||
path=_get(d, str, "path"),
|
||||
size=_get(d, int, "size"),
|
||||
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
|
||||
)
|
||||
_validate_path_url(package_wheel.path, package_wheel.url)
|
||||
return package_wheel
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
"""Get the filename of the wheel."""
|
||||
filename = self.name or _path_name(self.path) or _url_name(self.url)
|
||||
if not filename:
|
||||
raise PylockValidationError("Cannot determine wheel filename")
|
||||
return filename
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class Package:
|
||||
name: NormalizedName
|
||||
version: Version | None = None
|
||||
marker: Marker | None = None
|
||||
requires_python: SpecifierSet | None = None
|
||||
dependencies: Sequence[Mapping[str, Any]] | None = None
|
||||
vcs: PackageVcs | None = None
|
||||
directory: PackageDirectory | None = None
|
||||
archive: PackageArchive | None = None
|
||||
index: str | None = None
|
||||
sdist: PackageSdist | None = None
|
||||
wheels: Sequence[PackageWheel] | None = None
|
||||
attestation_identities: Sequence[Mapping[str, Any]] | None = None
|
||||
tool: Mapping[str, Any] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: NormalizedName,
|
||||
version: Version | None = None,
|
||||
marker: Marker | None = None,
|
||||
requires_python: SpecifierSet | None = None,
|
||||
dependencies: Sequence[Mapping[str, Any]] | None = None,
|
||||
vcs: PackageVcs | None = None,
|
||||
directory: PackageDirectory | None = None,
|
||||
archive: PackageArchive | None = None,
|
||||
index: str | None = None,
|
||||
sdist: PackageSdist | None = None,
|
||||
wheels: Sequence[PackageWheel] | None = None,
|
||||
attestation_identities: Sequence[Mapping[str, Any]] | None = None,
|
||||
tool: Mapping[str, Any] | None = None,
|
||||
) -> None:
|
||||
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||
object.__setattr__(self, "name", name)
|
||||
object.__setattr__(self, "version", version)
|
||||
object.__setattr__(self, "marker", marker)
|
||||
object.__setattr__(self, "requires_python", requires_python)
|
||||
object.__setattr__(self, "dependencies", dependencies)
|
||||
object.__setattr__(self, "vcs", vcs)
|
||||
object.__setattr__(self, "directory", directory)
|
||||
object.__setattr__(self, "archive", archive)
|
||||
object.__setattr__(self, "index", index)
|
||||
object.__setattr__(self, "sdist", sdist)
|
||||
object.__setattr__(self, "wheels", wheels)
|
||||
object.__setattr__(self, "attestation_identities", attestation_identities)
|
||||
object.__setattr__(self, "tool", tool)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
package = cls(
|
||||
name=_get_required_as(d, str, _validate_normalized_name, "name"),
|
||||
version=_get_as(d, str, Version, "version"),
|
||||
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
|
||||
dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract]
|
||||
marker=_get_as(d, str, Marker, "marker"),
|
||||
vcs=_get_object(d, PackageVcs, "vcs"),
|
||||
directory=_get_object(d, PackageDirectory, "directory"),
|
||||
archive=_get_object(d, PackageArchive, "archive"),
|
||||
index=_get(d, str, "index"),
|
||||
sdist=_get_object(d, PackageSdist, "sdist"),
|
||||
wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"),
|
||||
attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract]
|
||||
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
|
||||
)
|
||||
distributions = bool(package.sdist) + len(package.wheels or [])
|
||||
direct_urls = (
|
||||
bool(package.vcs) + bool(package.directory) + bool(package.archive)
|
||||
)
|
||||
if distributions > 0 and direct_urls > 0:
|
||||
raise PylockValidationError(
|
||||
"None of vcs, directory, archive must be set if sdist or wheels are set"
|
||||
)
|
||||
if distributions == 0 and direct_urls != 1:
|
||||
raise PylockValidationError(
|
||||
"Exactly one of vcs, directory, archive must be set "
|
||||
"if sdist and wheels are not set"
|
||||
)
|
||||
for i, wheel in enumerate(package.wheels or []):
|
||||
try:
|
||||
(name, version, _, _) = parse_wheel_filename(wheel.filename)
|
||||
except Exception as e:
|
||||
raise PylockValidationError(
|
||||
f"Invalid wheel filename {wheel.filename!r}",
|
||||
context=f"wheels[{i}]",
|
||||
) from e
|
||||
if name != package.name:
|
||||
raise PylockValidationError(
|
||||
f"Name in {wheel.filename!r} is not consistent with "
|
||||
f"package name {package.name!r}",
|
||||
context=f"wheels[{i}]",
|
||||
)
|
||||
if package.version and version != package.version:
|
||||
raise PylockValidationError(
|
||||
f"Version in {wheel.filename!r} is not consistent with "
|
||||
f"package version {str(package.version)!r}",
|
||||
context=f"wheels[{i}]",
|
||||
)
|
||||
if package.sdist:
|
||||
try:
|
||||
name, version = parse_sdist_filename(package.sdist.filename)
|
||||
except Exception as e:
|
||||
raise PylockValidationError(
|
||||
f"Invalid sdist filename {package.sdist.filename!r}",
|
||||
context="sdist",
|
||||
) from e
|
||||
if name != package.name:
|
||||
raise PylockValidationError(
|
||||
f"Name in {package.sdist.filename!r} is not consistent with "
|
||||
f"package name {package.name!r}",
|
||||
context="sdist",
|
||||
)
|
||||
if package.version and version != package.version:
|
||||
raise PylockValidationError(
|
||||
f"Version in {package.sdist.filename!r} is not consistent with "
|
||||
f"package version {str(package.version)!r}",
|
||||
context="sdist",
|
||||
)
|
||||
try:
|
||||
for i, attestation_identity in enumerate( # noqa: B007
|
||||
package.attestation_identities or []
|
||||
):
|
||||
_get_required(attestation_identity, str, "kind")
|
||||
except Exception as e:
|
||||
raise PylockValidationError(
|
||||
e, context=f"attestation-identities[{i}]"
|
||||
) from e
|
||||
return package
|
||||
|
||||
@property
|
||||
def is_direct(self) -> bool:
|
||||
return not (self.sdist or self.wheels)
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class Pylock:
|
||||
"""A class representing a pylock file."""
|
||||
|
||||
lock_version: Version
|
||||
environments: Sequence[Marker] | None = None
|
||||
requires_python: SpecifierSet | None = None
|
||||
extras: Sequence[NormalizedName] | None = None
|
||||
dependency_groups: Sequence[str] | None = None
|
||||
default_groups: Sequence[str] | None = None
|
||||
created_by: str # type: ignore[misc]
|
||||
packages: Sequence[Package] # type: ignore[misc]
|
||||
tool: Mapping[str, Any] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
lock_version: Version,
|
||||
environments: Sequence[Marker] | None = None,
|
||||
requires_python: SpecifierSet | None = None,
|
||||
extras: Sequence[NormalizedName] | None = None,
|
||||
dependency_groups: Sequence[str] | None = None,
|
||||
default_groups: Sequence[str] | None = None,
|
||||
created_by: str,
|
||||
packages: Sequence[Package],
|
||||
tool: Mapping[str, Any] | None = None,
|
||||
) -> None:
|
||||
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||
object.__setattr__(self, "lock_version", lock_version)
|
||||
object.__setattr__(self, "environments", environments)
|
||||
object.__setattr__(self, "requires_python", requires_python)
|
||||
object.__setattr__(self, "extras", extras)
|
||||
object.__setattr__(self, "dependency_groups", dependency_groups)
|
||||
object.__setattr__(self, "default_groups", default_groups)
|
||||
object.__setattr__(self, "created_by", created_by)
|
||||
object.__setattr__(self, "packages", packages)
|
||||
object.__setattr__(self, "tool", tool)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
pylock = cls(
|
||||
lock_version=_get_required_as(d, str, Version, "lock-version"),
|
||||
environments=_get_sequence_as(d, str, Marker, "environments"),
|
||||
extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"),
|
||||
dependency_groups=_get_sequence(d, str, "dependency-groups"),
|
||||
default_groups=_get_sequence(d, str, "default-groups"),
|
||||
created_by=_get_required(d, str, "created-by"),
|
||||
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
|
||||
packages=_get_required_sequence_of_objects(d, Package, "packages"),
|
||||
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
|
||||
)
|
||||
if not Version("1") <= pylock.lock_version < Version("2"):
|
||||
raise PylockUnsupportedVersionError(
|
||||
f"pylock version {pylock.lock_version} is not supported"
|
||||
)
|
||||
if pylock.lock_version > Version("1.0"):
|
||||
_logger.warning(
|
||||
"pylock minor version %s is not supported", pylock.lock_version
|
||||
)
|
||||
return pylock
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
|
||||
"""Create and validate a Pylock instance from a TOML dictionary.
|
||||
|
||||
Raises :class:`PylockValidationError` if the input data is not
|
||||
spec-compliant.
|
||||
"""
|
||||
return cls._from_dict(d)
|
||||
|
||||
def to_dict(self) -> Mapping[str, Any]:
|
||||
"""Convert the Pylock instance to a TOML dictionary."""
|
||||
return dataclasses.asdict(self, dict_factory=_toml_dict_factory)
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate the Pylock instance against the specification.
|
||||
|
||||
Raises :class:`PylockValidationError` otherwise."""
|
||||
self.from_dict(self.to_dict())
|
||||
|
||||
def select(
|
||||
self,
|
||||
*,
|
||||
environment: Environment | None = None,
|
||||
tags: Sequence[Tag] | None = None,
|
||||
extras: Collection[str] | None = None,
|
||||
dependency_groups: Collection[str] | None = None,
|
||||
) -> Iterator[
|
||||
tuple[
|
||||
Package,
|
||||
PackageVcs
|
||||
| PackageDirectory
|
||||
| PackageArchive
|
||||
| PackageWheel
|
||||
| PackageSdist,
|
||||
]
|
||||
]:
|
||||
"""Select what to install from the lock file.
|
||||
|
||||
The *environment* and *tags* parameters represent the environment being
|
||||
selected for. If unspecified, ``packaging.markers.default_environment()`` and
|
||||
``packaging.tags.sys_tags()`` are used.
|
||||
|
||||
The *extras* parameter represents the extras to install.
|
||||
|
||||
The *dependency_groups* parameter represents the groups to install. If
|
||||
unspecified, the default groups are used.
|
||||
|
||||
This method must be used on valid Pylock instances (i.e. one obtained
|
||||
from :meth:`Pylock.from_dict` or if constructed manually, after calling
|
||||
:meth:`Pylock.validate`).
|
||||
"""
|
||||
compatible_tags_selector = create_compatible_tags_selector(tags or sys_tags())
|
||||
|
||||
# #. Gather the extras and dependency groups to install and set ``extras`` and
|
||||
# ``dependency_groups`` for marker evaluation, respectively.
|
||||
#
|
||||
# #. ``extras`` SHOULD be set to the empty set by default.
|
||||
# #. ``dependency_groups`` SHOULD be the set created from
|
||||
# :ref:`pylock-default-groups` by default.
|
||||
env = cast(
|
||||
"dict[str, str | frozenset[str]]",
|
||||
dict(
|
||||
environment or {}, # Marker.evaluate will fill-up
|
||||
extras=frozenset(extras or []),
|
||||
dependency_groups=frozenset(
|
||||
(self.default_groups or [])
|
||||
if dependency_groups is None # to allow selecting no group
|
||||
else dependency_groups
|
||||
),
|
||||
),
|
||||
)
|
||||
env_python_full_version = (
|
||||
environment["python_full_version"]
|
||||
if environment
|
||||
else default_environment()["python_full_version"]
|
||||
)
|
||||
|
||||
# #. Check if the metadata version specified by :ref:`pylock-lock-version` is
|
||||
# supported; an error or warning MUST be raised as appropriate.
|
||||
# Covered by lock.validate() which is a precondition for this method.
|
||||
|
||||
# #. If :ref:`pylock-requires-python` is specified, check that the environment
|
||||
# being installed for meets the requirement; an error MUST be raised if it is
|
||||
# not met.
|
||||
if self.requires_python and not self.requires_python.contains(
|
||||
env_python_full_version,
|
||||
):
|
||||
raise PylockSelectError(
|
||||
f"python_full_version {env_python_full_version!r} "
|
||||
f"in provided environment does not satisfy the Python version "
|
||||
f"requirement {str(self.requires_python)!r}"
|
||||
)
|
||||
|
||||
# #. If :ref:`pylock-environments` is specified, check that at least one of the
|
||||
# environment marker expressions is satisfied; an error MUST be raised if no
|
||||
# expression is satisfied.
|
||||
if self.environments:
|
||||
for env_marker in self.environments:
|
||||
if env_marker.evaluate(
|
||||
cast("dict[str, str]", environment or {}), context="requirement"
|
||||
):
|
||||
break
|
||||
else:
|
||||
raise PylockSelectError(
|
||||
"Provided environment does not satisfy any of the "
|
||||
"environments specified in the lock file"
|
||||
)
|
||||
|
||||
# #. For each package listed in :ref:`pylock-packages`:
|
||||
selected_packages_by_name: dict[str, tuple[int, Package]] = {}
|
||||
for package_index, package in enumerate(self.packages):
|
||||
# #. If :ref:`pylock-packages-marker` is specified, check if it is
|
||||
# satisfied;if it isn't, skip to the next package.
|
||||
if package.marker and not package.marker.evaluate(env, context="lock_file"):
|
||||
continue
|
||||
|
||||
# #. If :ref:`pylock-packages-requires-python` is specified, check if it is
|
||||
# satisfied; an error MUST be raised if it isn't.
|
||||
if package.requires_python and not package.requires_python.contains(
|
||||
env_python_full_version,
|
||||
):
|
||||
raise PylockSelectError(
|
||||
f"python_full_version {env_python_full_version!r} "
|
||||
f"in provided environment does not satisfy the Python version "
|
||||
f"requirement {str(package.requires_python)!r} for package "
|
||||
f"{package.name!r} at packages[{package_index}]"
|
||||
)
|
||||
|
||||
# #. Check that no other conflicting instance of the package has been slated
|
||||
# to be installed; an error about the ambiguity MUST be raised otherwise.
|
||||
if package.name in selected_packages_by_name:
|
||||
raise PylockSelectError(
|
||||
f"Multiple packages with the name {package.name!r} are "
|
||||
f"selected at packages[{package_index}] and "
|
||||
f"packages[{selected_packages_by_name[package.name][0]}]"
|
||||
)
|
||||
|
||||
# #. Check that the source of the package is specified appropriately (i.e.
|
||||
# there are no conflicting sources in the package entry);
|
||||
# an error MUST be raised if any issues are found.
|
||||
# Covered by lock.validate() which is a precondition for this method.
|
||||
|
||||
# #. Add the package to the set of packages to install.
|
||||
selected_packages_by_name[package.name] = (package_index, package)
|
||||
|
||||
# #. For each package to be installed:
|
||||
for package_index, package in selected_packages_by_name.values():
|
||||
# - If :ref:`pylock-packages-vcs` is set:
|
||||
if package.vcs is not None:
|
||||
yield package, package.vcs
|
||||
|
||||
# - Else if :ref:`pylock-packages-directory` is set:
|
||||
elif package.directory is not None:
|
||||
yield package, package.directory
|
||||
|
||||
# - Else if :ref:`pylock-packages-archive` is set:
|
||||
elif package.archive is not None:
|
||||
yield package, package.archive
|
||||
|
||||
# - Else if there are entries for :ref:`pylock-packages-wheels`:
|
||||
elif package.wheels:
|
||||
# #. Look for the appropriate wheel file based on
|
||||
# :ref:`pylock-packages-wheels-name`; if one is not found then move
|
||||
# on to :ref:`pylock-packages-sdist` or an error MUST be raised about
|
||||
# a lack of source for the project.
|
||||
best_wheel = next(
|
||||
compatible_tags_selector(
|
||||
(wheel, parse_wheel_filename(wheel.filename)[-1])
|
||||
for wheel in package.wheels
|
||||
),
|
||||
None,
|
||||
)
|
||||
if best_wheel:
|
||||
yield package, best_wheel
|
||||
elif package.sdist is not None:
|
||||
yield package, package.sdist
|
||||
else:
|
||||
raise PylockSelectError(
|
||||
f"No wheel found matching the provided tags "
|
||||
f"for package {package.name!r} "
|
||||
f"at packages[{package_index}], "
|
||||
f"and no sdist available as a fallback"
|
||||
)
|
||||
|
||||
# - Else if no :ref:`pylock-packages-wheels` file is found or
|
||||
# :ref:`pylock-packages-sdist` is solely set:
|
||||
elif package.sdist is not None:
|
||||
yield package, package.sdist
|
||||
|
||||
else:
|
||||
# Covered by lock.validate() which is a precondition for this method.
|
||||
raise NotImplementedError # pragma: no cover
|
||||
@@ -3,7 +3,7 @@
|
||||
# for complete details.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterator
|
||||
from typing import Iterator
|
||||
|
||||
from ._parser import parse_requirement as _parse_requirement
|
||||
from ._tokenizer import ParserSyntaxError
|
||||
@@ -11,6 +11,15 @@ from .markers import Marker, _normalize_extra_values
|
||||
from .specifiers import SpecifierSet
|
||||
from .utils import canonicalize_name
|
||||
|
||||
__all__ = [
|
||||
"InvalidRequirement",
|
||||
"Requirement",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
class InvalidRequirement(ValueError):
|
||||
"""
|
||||
@@ -24,6 +33,16 @@ class Requirement:
|
||||
Parse a given requirement string into its parts, such as name, specifier,
|
||||
URL, and extras. Raises InvalidRequirement on a badly-formed requirement
|
||||
string.
|
||||
|
||||
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||
format so the same pickle can be loaded in future packaging releases.
|
||||
|
||||
.. versionchanged:: 26.2
|
||||
|
||||
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||
be unpickled with future releases. Backward compatibility with pickles
|
||||
from packaging < 26.2 is supported but may be removed in a future
|
||||
release.
|
||||
"""
|
||||
|
||||
# TODO: Can we test whether something is contained within a requirement?
|
||||
@@ -57,28 +76,47 @@ class Requirement:
|
||||
yield str(self.specifier)
|
||||
|
||||
if self.url:
|
||||
yield f"@ {self.url}"
|
||||
yield f" @ {self.url}"
|
||||
if self.marker:
|
||||
yield " "
|
||||
|
||||
if self.marker:
|
||||
yield f"; {self.marker}"
|
||||
|
||||
def __getstate__(self) -> str:
|
||||
# Return the requirement string for compactness and stability.
|
||||
# Re-parsed on load to reconstruct all fields.
|
||||
return str(self)
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, str):
|
||||
# New format (26.2+): just the requirement string.
|
||||
try:
|
||||
tmp = Requirement(state)
|
||||
except InvalidRequirement as exc:
|
||||
raise TypeError(f"Cannot restore Requirement from {state!r}") from exc
|
||||
self.name = tmp.name
|
||||
self.url = tmp.url
|
||||
self.extras = tmp.extras
|
||||
self.specifier = tmp.specifier
|
||||
self.marker = tmp.marker
|
||||
return
|
||||
if isinstance(state, dict):
|
||||
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
|
||||
self.__dict__.update(state)
|
||||
return
|
||||
raise TypeError(f"Cannot restore Requirement from {state!r}")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "".join(self._iter_parts(self.name))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Requirement('{self}')>"
|
||||
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(
|
||||
(
|
||||
self.__class__.__name__,
|
||||
*self._iter_parts(canonicalize_name(self.name)),
|
||||
)
|
||||
)
|
||||
return hash(tuple(self._iter_parts(canonicalize_name(self.name))))
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Requirement):
|
||||
return NotImplemented
|
||||
|
||||
|
||||
+1167
-243
File diff suppressed because it is too large
Load Diff
+337
-61
@@ -5,6 +5,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import platform
|
||||
import re
|
||||
import struct
|
||||
@@ -13,19 +14,52 @@ import sys
|
||||
import sysconfig
|
||||
from importlib.machinery import EXTENSION_SUFFIXES
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
from . import _manylinux, _musllinux
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import AbstractSet
|
||||
|
||||
|
||||
__all__ = [
|
||||
"INTERPRETER_SHORT_NAMES",
|
||||
"AppleVersion",
|
||||
"PythonVersion",
|
||||
"Tag",
|
||||
"UnsortedTagsError",
|
||||
"android_platforms",
|
||||
"compatible_tags",
|
||||
"cpython_tags",
|
||||
"create_compatible_tags_selector",
|
||||
"generic_tags",
|
||||
"interpreter_name",
|
||||
"interpreter_version",
|
||||
"ios_platforms",
|
||||
"mac_platforms",
|
||||
"parse_tag",
|
||||
"platform_tags",
|
||||
"sys_tags",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PythonVersion = Sequence[int]
|
||||
AppleVersion = Tuple[int, int]
|
||||
_T = TypeVar("_T")
|
||||
|
||||
INTERPRETER_SHORT_NAMES: dict[str, str] = {
|
||||
"python": "py", # Generic.
|
||||
@@ -36,7 +70,19 @@ INTERPRETER_SHORT_NAMES: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
_32_BIT_INTERPRETER = struct.calcsize("P") == 4
|
||||
# This function can be unit tested without reloading the module
|
||||
# (Unlike _32_BIT_INTERPRETER)
|
||||
def _compute_32_bit_interpreter() -> bool:
|
||||
return struct.calcsize("P") == 4
|
||||
|
||||
|
||||
_32_BIT_INTERPRETER = _compute_32_bit_interpreter()
|
||||
|
||||
|
||||
class UnsortedTagsError(ValueError):
|
||||
"""
|
||||
Raised when a tag component is not in sorted order per PEP 425.
|
||||
"""
|
||||
|
||||
|
||||
class Tag:
|
||||
@@ -45,11 +91,29 @@ class Tag:
|
||||
|
||||
Instances are considered immutable and thus are hashable. Equality checking
|
||||
is also supported.
|
||||
|
||||
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||
format so the same pickle can be loaded in future packaging releases.
|
||||
|
||||
.. versionchanged:: 26.2
|
||||
|
||||
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||
be unpickled with future releases. Backward compatibility with pickles
|
||||
from packaging < 26.2 is supported but may be removed in a future
|
||||
release.
|
||||
"""
|
||||
|
||||
__slots__ = ["_abi", "_hash", "_interpreter", "_platform"]
|
||||
|
||||
def __init__(self, interpreter: str, abi: str, platform: str) -> None:
|
||||
"""
|
||||
:param str interpreter: The interpreter name, e.g. ``"py"``
|
||||
(see :attr:`INTERPRETER_SHORT_NAMES` for mapping
|
||||
well-known interpreter names to their short names).
|
||||
:param str abi: The ABI that a wheel supports, e.g. ``"cp37m"``.
|
||||
:param str platform: The OS/platform the wheel supports,
|
||||
e.g. ``"win_amd64"``.
|
||||
"""
|
||||
self._interpreter = interpreter.lower()
|
||||
self._abi = abi.lower()
|
||||
self._platform = platform.lower()
|
||||
@@ -62,14 +126,25 @@ class Tag:
|
||||
|
||||
@property
|
||||
def interpreter(self) -> str:
|
||||
"""
|
||||
The interpreter name, e.g. ``"py"`` (see
|
||||
:attr:`INTERPRETER_SHORT_NAMES` for mapping well-known interpreter
|
||||
names to their short names).
|
||||
"""
|
||||
return self._interpreter
|
||||
|
||||
@property
|
||||
def abi(self) -> str:
|
||||
"""
|
||||
The supported ABI.
|
||||
"""
|
||||
return self._abi
|
||||
|
||||
@property
|
||||
def platform(self) -> str:
|
||||
"""
|
||||
The OS/platform.
|
||||
"""
|
||||
return self._platform
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
@@ -92,16 +167,69 @@ class Tag:
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self} @ {id(self)}>"
|
||||
|
||||
def __getstate__(self) -> tuple[str, str, str]:
|
||||
# Return state as a 3-item tuple: (interpreter, abi, platform).
|
||||
# Cache member _hash is excluded and will be recomputed.
|
||||
return (self._interpreter, self._abi, self._platform)
|
||||
|
||||
def parse_tag(tag: str) -> frozenset[Tag]:
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, tuple):
|
||||
if len(state) == 3 and all(isinstance(s, str) for s in state):
|
||||
# New format (26.2+): (interpreter, abi, platform)
|
||||
self._interpreter, self._abi, self._platform = state
|
||||
self._hash = hash((self._interpreter, self._abi, self._platform))
|
||||
return
|
||||
if len(state) == 2 and isinstance(state[1], dict):
|
||||
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
|
||||
_, slots = state
|
||||
try:
|
||||
interpreter = slots["_interpreter"]
|
||||
abi = slots["_abi"]
|
||||
platform = slots["_platform"]
|
||||
except KeyError:
|
||||
raise TypeError(f"Cannot restore Tag from {state!r}") from None
|
||||
if not all(
|
||||
isinstance(value, str) for value in (interpreter, abi, platform)
|
||||
):
|
||||
raise TypeError(f"Cannot restore Tag from {state!r}")
|
||||
self._interpreter = interpreter.lower()
|
||||
self._abi = abi.lower()
|
||||
self._platform = platform.lower()
|
||||
self._hash = hash((self._interpreter, self._abi, self._platform))
|
||||
return
|
||||
raise TypeError(f"Cannot restore Tag from {state!r}")
|
||||
|
||||
|
||||
def parse_tag(tag: str, *, validate_order: bool = False) -> frozenset[Tag]:
|
||||
"""
|
||||
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
|
||||
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of
|
||||
:class:`Tag` instances.
|
||||
|
||||
Returning a set is required due to the possibility that the tag is a
|
||||
compressed tag set.
|
||||
`compressed tag set`_, e.g. ``"py2.py3-none-any"`` which supports both
|
||||
Python 2 and Python 3.
|
||||
|
||||
If **validate_order** is true, compressed tag set components are checked
|
||||
to be in sorted order as required by PEP 425.
|
||||
|
||||
:param str tag: The tag to parse, e.g. ``"py3-none-any"``.
|
||||
:param bool validate_order: Check whether compressed tag set components
|
||||
are in sorted order.
|
||||
:raises UnsortedTagsError: If **validate_order** is true and any compressed tag
|
||||
set component is not in sorted order.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
The *validate_order* parameter.
|
||||
"""
|
||||
tags = set()
|
||||
interpreters, abis, platforms = tag.split("-")
|
||||
if validate_order:
|
||||
for component in (interpreters, abis, platforms):
|
||||
parts = component.split(".")
|
||||
if parts != sorted(parts):
|
||||
raise UnsortedTagsError(
|
||||
f"Tag component {component!r} is not in sorted order per PEP 425"
|
||||
)
|
||||
for interpreter in interpreters.split("."):
|
||||
for abi in abis.split("."):
|
||||
for platform_ in platforms.split("."):
|
||||
@@ -142,12 +270,25 @@ def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool:
|
||||
"""
|
||||
Determine if the Python version supports abi3.
|
||||
|
||||
PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`)
|
||||
PEP 384 was first implemented in Python 3.2. The free-threaded
|
||||
builds do not support abi3.
|
||||
"""
|
||||
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
|
||||
|
||||
|
||||
def _abi3t_applies(python_version: PythonVersion, threading: bool) -> bool:
|
||||
"""
|
||||
Determine if the Python version supports abi3t.
|
||||
|
||||
PEP 803 was first implemented in Python 3.15 but, per PEP 803, this
|
||||
returns tags going back to Python 3.2 to mirror the abi3
|
||||
implementation and leave open the possibility of abi3t wheels
|
||||
supporting older Python versions.
|
||||
|
||||
"""
|
||||
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and threading
|
||||
|
||||
|
||||
def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]:
|
||||
py_version = tuple(py_version) # To allow for version comparison.
|
||||
abis = []
|
||||
@@ -189,19 +330,31 @@ def cpython_tags(
|
||||
warn: bool = False,
|
||||
) -> Iterator[Tag]:
|
||||
"""
|
||||
Yields the tags for a CPython interpreter.
|
||||
Yields the tags for the CPython interpreter.
|
||||
|
||||
The tags consist of:
|
||||
- cp<python_version>-<abi>-<platform>
|
||||
- cp<python_version>-abi3-<platform>
|
||||
- cp<python_version>-none-<platform>
|
||||
- cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2.
|
||||
The specific tags generated are:
|
||||
|
||||
If python_version only specifies a major version then user-provided ABIs and
|
||||
the 'none' ABItag will be used.
|
||||
- ``cp<python_version>-<abi>-<platform>``
|
||||
- ``cp<python_version>-<stable_abi>-<platform>``
|
||||
- ``cp<python_version>-none-<platform>``
|
||||
- ``cp<older version>-<stable_abi>-<platform>`` where "older version" is all older
|
||||
minor versions down to Python 3.2 (when ``abi3`` was introduced)
|
||||
|
||||
If 'abi3' or 'none' are specified in 'abis' then they will be yielded at
|
||||
their normal position and not at the beginning.
|
||||
If ``python_version`` only provides a major-only version then only
|
||||
user-provided ABIs via ``abis`` and the ``none`` ABI will be used.
|
||||
|
||||
The ``stable_abi`` will be either ``abi3`` or ``abi3t`` if `abi` is a
|
||||
GIL-enabled ABI like `"cp315"` or a free-threaded ABI like `"cp315t"`,
|
||||
respectively.
|
||||
|
||||
:param Sequence python_version: A one- or two-item sequence representing the
|
||||
targeted Python version. Defaults to
|
||||
``sys.version_info[:2]``.
|
||||
:param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
|
||||
compatible with the current system.
|
||||
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
|
||||
platforms compatible with the current system.
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
"""
|
||||
if not python_version:
|
||||
python_version = sys.version_info[:2]
|
||||
@@ -209,16 +362,13 @@ def cpython_tags(
|
||||
interpreter = f"cp{_version_nodot(python_version[:2])}"
|
||||
|
||||
if abis is None:
|
||||
if len(python_version) > 1:
|
||||
abis = _cpython_abis(python_version, warn)
|
||||
else:
|
||||
abis = []
|
||||
abis = _cpython_abis(python_version, warn) if len(python_version) > 1 else []
|
||||
abis = list(abis)
|
||||
# 'abi3' and 'none' are explicitly handled later.
|
||||
for explicit_abi in ("abi3", "none"):
|
||||
try:
|
||||
abis.remove(explicit_abi)
|
||||
except ValueError:
|
||||
except ValueError: # noqa: PERF203
|
||||
pass
|
||||
|
||||
platforms = list(platforms or platform_tags())
|
||||
@@ -228,16 +378,27 @@ def cpython_tags(
|
||||
|
||||
threading = _is_threaded_cpython(abis)
|
||||
use_abi3 = _abi3_applies(python_version, threading)
|
||||
if use_abi3:
|
||||
yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
|
||||
yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
|
||||
use_abi3t = _abi3t_applies(python_version, threading)
|
||||
|
||||
if use_abi3:
|
||||
yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
|
||||
if use_abi3t:
|
||||
yield from (Tag(interpreter, "abi3t", platform_) for platform_ in platforms)
|
||||
|
||||
yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
|
||||
|
||||
if use_abi3 or use_abi3t:
|
||||
for minor_version in range(python_version[1] - 1, 1, -1):
|
||||
for platform_ in platforms:
|
||||
version = _version_nodot((python_version[0], minor_version))
|
||||
interpreter = f"cp{version}"
|
||||
yield Tag(interpreter, "abi3", platform_)
|
||||
if use_abi3:
|
||||
yield Tag(interpreter, "abi3", platform_)
|
||||
if use_abi3t:
|
||||
# Support for abi3t was introduced in Python 3.15, but in
|
||||
# principle abi3t wheels are possible for older limited API
|
||||
# versions, so allow things like ("cp37", "abi3t", "platform")
|
||||
yield Tag(interpreter, "abi3t", platform_)
|
||||
|
||||
|
||||
def _generic_abi() -> list[str]:
|
||||
@@ -289,21 +450,31 @@ def generic_tags(
|
||||
warn: bool = False,
|
||||
) -> Iterator[Tag]:
|
||||
"""
|
||||
Yields the tags for a generic interpreter.
|
||||
Yields the tags for an interpreter which requires no specialization.
|
||||
|
||||
The tags consist of:
|
||||
- <interpreter>-<abi>-<platform>
|
||||
This function should be used if one of the other interpreter-specific
|
||||
functions provided by this module is not appropriate (i.e. not calculating
|
||||
tags for a CPython interpreter).
|
||||
|
||||
The "none" ABI will be added if it was not explicitly provided.
|
||||
The specific tags generated are:
|
||||
|
||||
- ``<interpreter>-<abi>-<platform>``
|
||||
|
||||
The ``"none"`` ABI will be added if it was not explicitly provided.
|
||||
|
||||
:param str interpreter: The name of the interpreter. Defaults to being
|
||||
calculated.
|
||||
:param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
|
||||
compatible with the current system.
|
||||
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
|
||||
platforms compatible with the current system.
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
"""
|
||||
if not interpreter:
|
||||
interp_name = interpreter_name()
|
||||
interp_version = interpreter_version(warn=warn)
|
||||
interpreter = "".join([interp_name, interp_version])
|
||||
if abis is None:
|
||||
abis = _generic_abi()
|
||||
else:
|
||||
abis = list(abis)
|
||||
interpreter = f"{interp_name}{interp_version}"
|
||||
abis = _generic_abi() if abis is None else list(abis)
|
||||
platforms = list(platforms or platform_tags())
|
||||
if "none" not in abis:
|
||||
abis.append("none")
|
||||
@@ -333,12 +504,22 @@ def compatible_tags(
|
||||
platforms: Iterable[str] | None = None,
|
||||
) -> Iterator[Tag]:
|
||||
"""
|
||||
Yields the sequence of tags that are compatible with a specific version of Python.
|
||||
Yields the tags for an interpreter compatible with the Python version
|
||||
specified by ``python_version``.
|
||||
|
||||
The tags consist of:
|
||||
- py*-none-<platform>
|
||||
- <interpreter>-none-any # ... if `interpreter` is provided.
|
||||
- py*-none-any
|
||||
The specific tags generated are:
|
||||
|
||||
- ``py*-none-<platform>``
|
||||
- ``<interpreter>-none-any`` if ``interpreter`` is provided
|
||||
- ``py*-none-any``
|
||||
|
||||
:param Sequence python_version: A one- or two-item sequence representing the
|
||||
compatible version of Python. Defaults to
|
||||
``sys.version_info[:2]``.
|
||||
:param str interpreter: The name of the interpreter (if known), e.g.
|
||||
``"cp38"``. Defaults to the current interpreter.
|
||||
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
|
||||
platforms compatible with the current system.
|
||||
"""
|
||||
if not python_version:
|
||||
python_version = sys.version_info[:2]
|
||||
@@ -398,12 +579,25 @@ def mac_platforms(
|
||||
version: AppleVersion | None = None, arch: str | None = None
|
||||
) -> Iterator[str]:
|
||||
"""
|
||||
Yields the platform tags for a macOS system.
|
||||
Yields the :attr:`~Tag.platform` tags for macOS.
|
||||
|
||||
The `version` parameter is a two-item tuple specifying the macOS version to
|
||||
generate platform tags for. The `arch` parameter is the CPU architecture to
|
||||
generate platform tags for. Both parameters default to the appropriate value
|
||||
for the current system.
|
||||
|
||||
:param tuple version: A two-item tuple representing the version of macOS.
|
||||
Defaults to the current system's version.
|
||||
:param str arch: The CPU architecture. Defaults to the architecture of the
|
||||
current system, e.g. ``"x86_64"``.
|
||||
|
||||
.. note::
|
||||
Equivalent support for the other major platforms is purposefully not
|
||||
provided:
|
||||
|
||||
- On Windows, platform compatibility is statically specified
|
||||
- On Linux, code must be run on the system itself to determine
|
||||
compatibility
|
||||
"""
|
||||
version_str, _, cpu_arch = platform.mac_ver()
|
||||
if version is None:
|
||||
@@ -424,14 +618,11 @@ def mac_platforms(
|
||||
text=True,
|
||||
).stdout
|
||||
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
|
||||
else:
|
||||
version = version
|
||||
|
||||
if arch is None:
|
||||
arch = _mac_arch(cpu_arch)
|
||||
else:
|
||||
arch = arch
|
||||
|
||||
if (10, 0) <= version and version < (11, 0):
|
||||
if (10, 0) <= version < (11, 0):
|
||||
# Prior to Mac OS 11, each yearly release of Mac OS bumped the
|
||||
# "minor" version number. The major version was always 10.
|
||||
major_version = 10
|
||||
@@ -477,14 +668,19 @@ def ios_platforms(
|
||||
version: AppleVersion | None = None, multiarch: str | None = None
|
||||
) -> Iterator[str]:
|
||||
"""
|
||||
Yields the platform tags for an iOS system.
|
||||
|
||||
:param version: A two-item tuple specifying the iOS version to generate
|
||||
platform tags for. Defaults to the current iOS version.
|
||||
:param multiarch: The CPU architecture+ABI to generate platform tags for -
|
||||
(the value used by `sys.implementation._multiarch` e.g.,
|
||||
`arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current
|
||||
multiarch value.
|
||||
Yields the :attr:`~Tag.platform` tags for iOS.
|
||||
|
||||
:param tuple version: A two-item tuple representing the version of iOS.
|
||||
Defaults to the current system's version.
|
||||
:param str multiarch: The CPU architecture+ABI to be used. This should be in
|
||||
the format by ``sys.implementation._multiarch`` (e.g.,
|
||||
``arm64_iphoneos`` or ``x86_64_iphonesimulator``).
|
||||
Defaults to the current system's multiarch value.
|
||||
|
||||
.. note::
|
||||
Behavior of this method is undefined if invoked on non-iOS platforms
|
||||
without providing explicit version and multiarch arguments.
|
||||
"""
|
||||
if version is None:
|
||||
# if iOS is the current platform, ios_ver *must* be defined. However,
|
||||
@@ -586,13 +782,22 @@ def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
|
||||
yield f"linux_{arch}"
|
||||
|
||||
|
||||
def _emscripten_platforms() -> Iterator[str]:
|
||||
pyemscripten_platform_version = sysconfig.get_config_var(
|
||||
"PYEMSCRIPTEN_PLATFORM_VERSION"
|
||||
)
|
||||
if pyemscripten_platform_version:
|
||||
yield f"pyemscripten_{pyemscripten_platform_version}_wasm32"
|
||||
yield from _generic_platforms()
|
||||
|
||||
|
||||
def _generic_platforms() -> Iterator[str]:
|
||||
yield _normalize_string(sysconfig.get_platform())
|
||||
|
||||
|
||||
def platform_tags() -> Iterator[str]:
|
||||
"""
|
||||
Provides the platform tags for this installation.
|
||||
Yields the :attr:`~Tag.platform` tags for the running interpreter.
|
||||
"""
|
||||
if platform.system() == "Darwin":
|
||||
return mac_platforms()
|
||||
@@ -602,6 +807,8 @@ def platform_tags() -> Iterator[str]:
|
||||
return android_platforms()
|
||||
elif platform.system() == "Linux":
|
||||
return _linux_platforms()
|
||||
elif platform.system() == "Emscripten":
|
||||
return _emscripten_platforms()
|
||||
else:
|
||||
return _generic_platforms()
|
||||
|
||||
@@ -612,6 +819,8 @@ def interpreter_name() -> str:
|
||||
|
||||
Some implementations have a reserved, two-letter abbreviation which will
|
||||
be returned when appropriate.
|
||||
|
||||
This typically acts as the prefix to the :attr:`~Tag.interpreter` tag.
|
||||
"""
|
||||
name = sys.implementation.name
|
||||
return INTERPRETER_SHORT_NAMES.get(name) or name
|
||||
@@ -619,14 +828,14 @@ def interpreter_name() -> str:
|
||||
|
||||
def interpreter_version(*, warn: bool = False) -> str:
|
||||
"""
|
||||
Returns the version of the running interpreter.
|
||||
Returns the running interpreter's version.
|
||||
|
||||
This typically acts as the suffix to the :attr:`~Tag.interpreter` tag.
|
||||
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
"""
|
||||
version = _get_config_var("py_version_nodot", warn=warn)
|
||||
if version:
|
||||
version = str(version)
|
||||
else:
|
||||
version = _version_nodot(sys.version_info[:2])
|
||||
return version
|
||||
return str(version) if version else _version_nodot(sys.version_info[:2])
|
||||
|
||||
|
||||
def _version_nodot(version: PythonVersion) -> str:
|
||||
@@ -635,10 +844,31 @@ def _version_nodot(version: PythonVersion) -> str:
|
||||
|
||||
def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
|
||||
"""
|
||||
Returns the sequence of tag triples for the running interpreter.
|
||||
Yields the sequence of tag triples that the running interpreter supports.
|
||||
|
||||
The order of the sequence corresponds to priority order for the
|
||||
interpreter, from most to least important.
|
||||
The iterable is ordered so that the best-matching tag is first in the
|
||||
sequence. The exact preferential order to tags is interpreter-specific, but
|
||||
in general the tag importance is in the order of:
|
||||
|
||||
1. Interpreter
|
||||
2. Platform
|
||||
3. ABI
|
||||
|
||||
This order is due to the fact that an ABI is inherently tied to the
|
||||
platform, but platform-specific code is not necessarily tied to the ABI. The
|
||||
interpreter is the most important tag as it dictates basic support for any
|
||||
wheel.
|
||||
|
||||
The function returns an iterable in order to allow for the possible
|
||||
short-circuiting of tag generation if the entire sequence is not necessary
|
||||
and tag calculation happens to be expensive.
|
||||
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
|
||||
.. versionchanged:: 21.3
|
||||
Added the `pp3-none-any` tag (:issue:`311`).
|
||||
.. versionchanged:: 27.0
|
||||
Added the `abi3t` tag (:issue:`1099`).
|
||||
"""
|
||||
|
||||
interp_name = interpreter_name()
|
||||
@@ -654,3 +884,49 @@ def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
|
||||
else:
|
||||
interp = None
|
||||
yield from compatible_tags(interpreter=interp)
|
||||
|
||||
|
||||
def create_compatible_tags_selector(
|
||||
tags: Iterable[Tag],
|
||||
) -> Callable[[Iterable[tuple[_T, AbstractSet[Tag]]]], Iterator[_T]]:
|
||||
"""Create a callable to select things compatible with supported tags.
|
||||
|
||||
This function accepts an ordered sequence of tags, with the preferred
|
||||
tags first.
|
||||
|
||||
The returned callable accepts an iterable of tuples (thing, set[Tag]),
|
||||
and returns an iterator of things, with the things with the best
|
||||
matching tags first.
|
||||
|
||||
Example to select compatible wheel filenames:
|
||||
|
||||
>>> from packaging import tags
|
||||
>>> from packaging.utils import parse_wheel_filename
|
||||
>>> selector = tags.create_compatible_tags_selector(tags.sys_tags())
|
||||
>>> filenames = ["foo-1.0-py3-none-any.whl", "foo-1.0-py2-none-any.whl"]
|
||||
>>> list(selector([
|
||||
... (filename, parse_wheel_filename(filename)[-1]) for filename in filenames
|
||||
... ]))
|
||||
['foo-1.0-py3-none-any.whl']
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
tag_ranks: dict[Tag, int] = {}
|
||||
for rank, tag in enumerate(tags):
|
||||
tag_ranks.setdefault(tag, rank) # ignore duplicate tags, keep first
|
||||
supported_tags = tag_ranks.keys()
|
||||
|
||||
def selector(
|
||||
tagged_things: Iterable[tuple[_T, AbstractSet[Tag]]],
|
||||
) -> Iterator[_T]:
|
||||
ranked_things: list[tuple[_T, int]] = []
|
||||
for thing, thing_tags in tagged_things:
|
||||
supported_thing_tags = thing_tags & supported_tags
|
||||
if supported_thing_tags:
|
||||
thing_rank = min(tag_ranks[t] for t in supported_thing_tags)
|
||||
ranked_things.append((thing, thing_rank))
|
||||
return iter(
|
||||
thing for thing, _ in sorted(ranked_things, key=operator.itemgetter(1))
|
||||
)
|
||||
|
||||
return selector
|
||||
|
||||
+160
-27
@@ -4,15 +4,36 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import re
|
||||
from typing import NewType, Tuple, Union, cast
|
||||
|
||||
from .tags import Tag, parse_tag
|
||||
from .tags import Tag, UnsortedTagsError, parse_tag
|
||||
from .version import InvalidVersion, Version, _TrimmedRelease
|
||||
|
||||
__all__ = [
|
||||
"BuildTag",
|
||||
"InvalidName",
|
||||
"InvalidSdistFilename",
|
||||
"InvalidWheelFilename",
|
||||
"NormalizedName",
|
||||
"canonicalize_name",
|
||||
"canonicalize_version",
|
||||
"is_normalized_name",
|
||||
"parse_sdist_filename",
|
||||
"parse_wheel_filename",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
BuildTag = Union[Tuple[()], Tuple[int, str]]
|
||||
|
||||
NormalizedName = NewType("NormalizedName", str)
|
||||
"""
|
||||
A :class:`typing.NewType` of :class:`str`, representing a normalized name.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidName(ValueError):
|
||||
@@ -35,33 +56,77 @@ class InvalidSdistFilename(ValueError):
|
||||
|
||||
# Core metadata spec for `Name`
|
||||
_validate_regex = re.compile(
|
||||
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE
|
||||
r"[a-z0-9]|[a-z0-9][a-z0-9._-]*[a-z0-9]", re.IGNORECASE | re.ASCII
|
||||
)
|
||||
_canonicalize_regex = re.compile(r"[-_.]+")
|
||||
_normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$")
|
||||
_normalized_regex = re.compile(r"[a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9]", re.ASCII)
|
||||
# PEP 427: The build number must start with a digit.
|
||||
_build_tag_regex = re.compile(r"(\d+)(.*)")
|
||||
_build_tag_regex = re.compile(r"(\d+)(.*)", re.ASCII)
|
||||
|
||||
|
||||
def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
|
||||
if validate and not _validate_regex.match(name):
|
||||
"""
|
||||
This function takes a valid Python package or extra name, and returns the
|
||||
normalized form of it.
|
||||
|
||||
The return type is typed as :class:`NormalizedName`. This allows type
|
||||
checkers to help require that a string has passed through this function
|
||||
before use.
|
||||
|
||||
If **validate** is true, then the function will check if **name** is a valid
|
||||
distribution name before normalizing.
|
||||
|
||||
:param str name: The name to normalize.
|
||||
:param bool validate: Check whether the name is a valid distribution name.
|
||||
:raises InvalidName: If **validate** is true and the name is not an
|
||||
acceptable distribution name.
|
||||
|
||||
>>> from packaging.utils import canonicalize_name
|
||||
>>> canonicalize_name("Django")
|
||||
'django'
|
||||
>>> canonicalize_name("oslo.concurrency")
|
||||
'oslo-concurrency'
|
||||
>>> canonicalize_name("requests")
|
||||
'requests'
|
||||
"""
|
||||
if validate and not _validate_regex.fullmatch(name):
|
||||
raise InvalidName(f"name is invalid: {name!r}")
|
||||
# This is taken from PEP 503.
|
||||
value = _canonicalize_regex.sub("-", name).lower()
|
||||
return cast(NormalizedName, value)
|
||||
# Ensure all ``.`` and ``_`` are ``-``
|
||||
# Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503
|
||||
# Much faster than re, and even faster than str.translate
|
||||
value = name.lower().replace("_", "-").replace(".", "-")
|
||||
# Condense repeats (faster than regex)
|
||||
while "--" in value:
|
||||
value = value.replace("--", "-")
|
||||
return cast("NormalizedName", value)
|
||||
|
||||
|
||||
def is_normalized_name(name: str) -> bool:
|
||||
return _normalized_regex.match(name) is not None
|
||||
"""
|
||||
Check if a name is already normalized (i.e. :func:`canonicalize_name` would
|
||||
roundtrip to the same value).
|
||||
|
||||
:param str name: The name to check.
|
||||
|
||||
>>> from packaging.utils import is_normalized_name
|
||||
>>> is_normalized_name("requests")
|
||||
True
|
||||
>>> is_normalized_name("Django")
|
||||
False
|
||||
"""
|
||||
return _normalized_regex.fullmatch(name) is not None
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
def canonicalize_version(
|
||||
version: Version | str, *, strip_trailing_zero: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Return a canonical form of a version as a string.
|
||||
"""Return a canonical form of a version as a string.
|
||||
|
||||
This function takes a string representing a package version (or a
|
||||
:class:`~packaging.version.Version` instance), and returns the
|
||||
normalized form of it. By default, it strips trailing zeros from
|
||||
the release segment.
|
||||
|
||||
>>> from packaging.utils import canonicalize_version
|
||||
>>> canonicalize_version('1.0.1')
|
||||
'1.0.1'
|
||||
|
||||
@@ -77,23 +142,63 @@ def canonicalize_version(
|
||||
|
||||
>>> canonicalize_version('foo bar baz')
|
||||
'foo bar baz'
|
||||
|
||||
>>> canonicalize_version('1.4.0.0.0')
|
||||
'1.4'
|
||||
"""
|
||||
return str(_TrimmedRelease(str(version)) if strip_trailing_zero else version)
|
||||
|
||||
|
||||
@canonicalize_version.register
|
||||
def _(version: str, *, strip_trailing_zero: bool = True) -> str:
|
||||
try:
|
||||
parsed = Version(version)
|
||||
except InvalidVersion:
|
||||
# Legacy versions cannot be normalized
|
||||
return version
|
||||
return canonicalize_version(parsed, strip_trailing_zero=strip_trailing_zero)
|
||||
if isinstance(version, str):
|
||||
try:
|
||||
version = Version(version)
|
||||
except InvalidVersion:
|
||||
return str(version)
|
||||
return str(_TrimmedRelease(version) if strip_trailing_zero else version)
|
||||
|
||||
|
||||
def parse_wheel_filename(
|
||||
filename: str,
|
||||
*,
|
||||
validate_order: bool = False,
|
||||
) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
|
||||
"""
|
||||
This function takes the filename of a wheel file, and parses it,
|
||||
returning a tuple of name, version, build number, and tags.
|
||||
|
||||
The name part of the tuple is normalized and typed as
|
||||
:class:`NormalizedName`. The version portion is an instance of
|
||||
:class:`~packaging.version.Version`. The build number is ``()`` if
|
||||
there is no build number in the wheel filename, otherwise a
|
||||
two-item tuple of an integer for the leading digits and
|
||||
a string for the rest of the build number. The tags portion is a
|
||||
frozen set of :class:`~packaging.tags.Tag` instances (as the tag
|
||||
string format allows multiple tags to be combined into a single
|
||||
string).
|
||||
|
||||
If **validate_order** is true, compressed tag set components are
|
||||
checked to be in sorted order as required by PEP 425.
|
||||
|
||||
:param str filename: The name of the wheel file.
|
||||
:param bool validate_order: Check whether compressed tag set components
|
||||
are in sorted order.
|
||||
:raises InvalidWheelFilename: If the filename in question
|
||||
does not follow the :ref:`wheel specification
|
||||
<pypug:binary-distribution-format>`.
|
||||
|
||||
>>> from packaging.utils import parse_wheel_filename
|
||||
>>> from packaging.tags import Tag
|
||||
>>> from packaging.version import Version
|
||||
>>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl")
|
||||
>>> name
|
||||
'foo'
|
||||
>>> ver == Version('1.0')
|
||||
True
|
||||
>>> tags == {Tag("py3", "none", "any")}
|
||||
True
|
||||
>>> not build
|
||||
True
|
||||
|
||||
.. versionadded:: 26.1
|
||||
The *validate_order* parameter.
|
||||
"""
|
||||
if not filename.endswith(".whl"):
|
||||
raise InvalidWheelFilename(
|
||||
f"Invalid wheel filename (extension must be '.whl'): {filename!r}"
|
||||
@@ -127,14 +232,42 @@ def parse_wheel_filename(
|
||||
raise InvalidWheelFilename(
|
||||
f"Invalid build number: {build_part} in {filename!r}"
|
||||
)
|
||||
build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2)))
|
||||
build = cast("BuildTag", (int(build_match.group(1)), build_match.group(2)))
|
||||
else:
|
||||
build = ()
|
||||
tags = parse_tag(parts[-1])
|
||||
tag_str = parts[-1]
|
||||
try:
|
||||
tags = parse_tag(tag_str, validate_order=validate_order)
|
||||
except UnsortedTagsError:
|
||||
raise InvalidWheelFilename(
|
||||
f"Invalid wheel filename (compressed tag set components must be in "
|
||||
f"sorted order per PEP 425): {filename!r}"
|
||||
) from None
|
||||
return (name, version, build, tags)
|
||||
|
||||
|
||||
def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
|
||||
"""
|
||||
This function takes the filename of a sdist file (as specified
|
||||
in the `Source distribution format`_ documentation), and parses
|
||||
it, returning a tuple of the normalized name and version as
|
||||
represented by an instance of :class:`~packaging.version.Version`.
|
||||
|
||||
:param str filename: The name of the sdist file.
|
||||
:raises InvalidSdistFilename: If the filename does not end
|
||||
with an sdist extension (``.zip`` or ``.tar.gz``), or if it does not
|
||||
contain a dash separating the name and the version of the distribution.
|
||||
|
||||
>>> from packaging.utils import parse_sdist_filename
|
||||
>>> from packaging.version import Version
|
||||
>>> name, ver = parse_sdist_filename("foo-1.0.tar.gz")
|
||||
>>> name
|
||||
'foo'
|
||||
>>> ver == Version('1.0')
|
||||
True
|
||||
|
||||
.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name
|
||||
"""
|
||||
if filename.endswith(".tar.gz"):
|
||||
file_stem = filename[: -len(".tar.gz")]
|
||||
elif filename.endswith(".zip"):
|
||||
|
||||
+803
-154
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
pip
|
||||
uv
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user