General approach for this kind of standards:
- Optimize for the most use-cases
- Provide a escape hatch for use-cases our general approach doesn't work
Python versions are defined by PEP 440, but here's a quick summary.
({epoch}!){release}({pre})({post})({dev})(+{local})
- Epoch (
epoch)- Way to override the release section, enabling forced downgrades (eg.
1!1.2.3is greater than2.0.0) - Format
- integer
- Way to override the release section, enabling forced downgrades (eg.
- Release (
release)- The main element of the release
- Format
.-separated list of integers (eg.1.2.3.4.5)- Must contain at least one element, and there's not limit for the number elements
- Pre-release modifier (
pre)- Makes the version a pre-release, meaning it comes before the normal version with the same release value (eg.
1.2.3.a0is less than1.2.3) - Format:
([-_.]){pre_letter}([-_.]){pre_number}(eg.a0,-a0,_a0,.a0,a.0,a-0,-a-0, etc.)- Letter (
pre_letter)- One of the following:
alpha,abeta,b,preview,precrc
- One of the following:
- Number (
pre_number)- integer
- Letter (
- Makes the version a pre-release, meaning it comes before the normal version with the same release value (eg.
- Post-release modifier
- Makes the version a post-release, meaning it comes after the normal version with the same release value (eg.
1.2.3.post0is less than1.2.3) - Format (one of)
-{post_number}(eg.-1)([-_.]){post_letter}([-_.]){post_number}(eg.r0,-r0,_r0,.r0,r.0,-0,-r-0, etc.)- Letter (
pre_letter)- One of the following:
post,rev,r
- One of the following:
- Number (
pre_number)- integer
- Letter (
- Makes the version a post-release, meaning it comes after the normal version with the same release value (eg.
- Dev-release modifier
- Makes the version a development release, meaning it comes before the normal version with the same release value (eg.
1.2.3.dev0is less than1.2.3) - Format:
([-_.])dev([-_.]){dev}(eg.dev0,-dev0,_dev0,.dev0,dev.0,dev-0,-dev-0)- Number (
dev)- integer
- Number (
- Makes the version a development release, meaning it comes before the normal version with the same release value (eg.
- Local identifier (
local)- Purely informational identifier, which can be use for eg. to differentiate artifacts with otherwise the same name
Note: integers, in this context, are whole numbers, and can be either single or multi-digit (eg. 1, 123, 989898989)
Python requirement strings are defined by PEP 508, but here's a quick summary.
{operator}{version}(;{env_marker})
- Operator (
operator)- Comparison operation to perform
- Defined by PEP 440
- Format (one of)
==,=!(matching/exclusion operators)- Perform an equals / not equals operation
- When using this operators, one of the release element might be a
*, denoting a wildcard (eg.== 1.2.*,!= 1.2.*)
<,>,<=,>=- Perform a greater/less than/or equal operation
===- Performs an arbitrary equality operation (only matches against the exact same version)
~=- Performs a "compatible" release operation
- Equivalent to a greater than operation on the specified version, and a matching operation on the specified version with a wildcard on the last element (eg.
~= X.Ywould be equivalent to>= X.Y, == X.*,~= 1.2.3.4.5would be equivalent to>= 1.2.3.4.5, == 1.2.3.4.*)
- Equivalent to a greater than operation on the specified version, and a matching operation on the specified version with a wildcard on the last element (eg.
- Cannot be used on versions single element releases (eg.
~= 1would not be legal ) - Cannot be used on versions with local identifiers (eg.
~= 1.2.3+somethingwould not be legal)
- Performs a "compatible" release operation
- Version (
version)- Format
- A PEP 440 version (described above)
- Format
- Environment marker (
env_marker)- Makes the requirement string optional, based on some environment information.
- Format
The release section of the version is matched using @ as a placeholder for release parts (eg. @.@.@).
All PEP 440 operators but != can be used (!= never makes sense when matching without arithmetic expressions).
If a dev or pre version is specified, the original version will be pinned, even if a wildcard is used (such cases will be logged with a info level).
-
1.2.3on== @.@.@>== 1.2.3 -
1.2.3.4.5on== @.@.@>== 1.2.3 -
1.2.3.post0on>= @.@.@>>= 1.2.3 -
1.2.3.dev0on== @.@.@>== 1.2.3.dev0 -
1.2.3.dev0on== @.@.*>== 1.2.3.dev0 -
1.2.3.dev0on>= @.@.@>== 1.2.3.dev0 -
1.2.3.dev0on>= @>== 1.2.3.dev0(errors)
-
1.2on== @.@.@== @.@.@?can be an option to work around this, making the 3rd part optional, but I don't think there's enough need
-
Matching
-
@matching- On the release section (eg.
@.@.@on1.2.3.4.5>1.2.3)- This makes dealing with the variable number of elements very easy
- On the pre/post/dev sections (eg.
@.@[email protected]@on1.2.3.dev@>1.2.3.dev0)- Semantics are very tricky, needs more info (XXX)
- What about
@.@[email protected]@on1.2.3.4.5.dev0?- Should we allow this? (first instinct: no)
- What about
- Semantics are very tricky, needs more info (XXX)
- On the release section (eg.
-
Optional matching
- Allow matching only if a section is present
- Eg.
@.@[email protected]@?1.2.3>1.2.31.2.3.dev0>1.2.3.dev01.2.3.4.5>1.2.31.2.3.4.5.dev0>1.2.3.dev0- Should we allow this? (first instinct: no)
- How do we handle the letter section in pre-releases (eg.
1.2.3.alpha0and1.2.3.beta0)
- Eg.
- Allow matching only if a section is present
-
Conditional matching
- Add a conditional to enable a certain constrain, similar to environment markers
- Examples:
@.@.@ ; is_dev@.@.@ ; >= 1.2.3- Good for version changes
- Examples:
- Add a conditional to enable a certain constrain, similar to environment markers
-
Backup matching
[tool.mesonpy.binary-runtime-constrains] packageA = 'match(@.@.@)' packageB = ['match(@.@.@)', 'match(@.@[email protected])'] packageC = ['match(@.@.@)', 'exact']
- Strategy
- If the value is a string, match it
- If the value is a list, match the first element, if the resulting requirement string is valid for the matched version, use it, otherwise go to the next element
- Strategy
-
Custom strategy matching
- We define certain matching strategies, which the users can use. Strategies can take arguments.
exactcompatible(...)- XXX: How to specify the version?
compatible(3)(1.2.3.4.5>~= 1.2.3)compatible(@.@.@)(1.2.3.4.5>~= 1.2.3)
- XXX: How to specify the version?
- We define certain matching strategies, which the users can use. Strategies can take arguments.
-
Just do it in code 😅
-
If none of these approaches are deemed good enough, or if matching is too complex for the other approaches, we can just add the runtime dependency constrain as a code option
- This does not get rid of any of the issues, it just throws them to the user
- Some of them may be more equipped to deal with these issues, but some probably aren't
- If the logic is tricky (eg. what to match when a dev release is used), users will almost definitely mess it up
- If we can avoid making users having to deal with this, we should probably do it
- Optimizing for the most common use-cases might not be viable though
- If we can avoid making users having to deal with this, we should probably do it
- This does not get rid of any of the issues, it just throws them to the user
-
Users create a
meson-python-config.py(or a better name) and do the matching themselvesfrom collections.abc import Iterator from mesonpy.types import Version def runtime_constrains(name: str, version: Version) -> Iterator[str]: if name == 'packageA': yield f'~= {version.release[:3]}' elif name == 'packageB': if version.dev: yield f'== {version}' else: yield f'~= {version.release[:3]}' elif name == 'packageC': if version >= Version('1.2.3.4'): yield f'~= {version.release[:4]}' else: yield f'~= {version.release[:3]}'
-
-
-
Matching issues
- Failed matching check
- A matching pattern can succeed, but it might fail to match against the version it was matched against
- This only happens when we allow patching to dev/pre/post releases
- A matching pattern can succeed, but it might fail to match against the version it was matched against
- Failed matching check
-
Format
-
Full strings
[tool.mesonpy] binary-runtime-constrains = [ 'packageA = match(@.@.@)', 'packageB = exact', 'packageC = custom:identifier(args)', ]
- Allows multiple constrains for the same package
-
String-mapping
[tool.mesonpy.binary-runtime-constrains] packageA = 'match(@.@.@)' packageB = 'exact' packageC = 'custom:identifier(args)'
-
Dict-mapping w/ inferred string default
[tool.mesonpy.binary-runtime-constrains] packageA = '@.@.@' packageB = {strategy='exact'} packageC = {strategy='custom:identifier' args=...}
- TOML doesn't support multi-line
{}-style mappings, so this would be a but unergonomic on more complex cases
- TOML doesn't support multi-line
-
@-matching is great for matching the release section- We probably want different matching logic for pre/dev versions
- The most sane approach would probably be to forcibly pin those versions
- The post part of versions is basically an annoying "minor" release part
This details some of the examples we want to support.
This is the most common use-case, matching parts of the
- A:
~= @.@.@
- A:
~= @.@.@
In these cases, we should probably pin the dev release, == 1.28.0.dev0.
- A1: Custom mapper, the recommended way with
@-matching would be~= @.@.@, which translates into== 1.27.0.dev0
In these cases, we'd probably want to match to >= 1.27.0.alpha0, < 1.27.0
- A1: Custom mapper, the recommended way with
@-matching would be~= @.@.@, which translates into== 1.27.0.alpha0