ganzua constraints bump
Usage: ganzua constraints bump [OPTIONS] [PYPROJECT]
Update pyproject.toml dependency constraints to match the lockfile.
Of course, the lockfile should always be a valid solution for the constraints. But often, the constraints are somewhat relaxed. This tool will increment the constraints to match the currently locked versions. Specifically, the locked version becomes a lower bound for the constraint.
This tool will try to be as granular as the original constraint.
For example, given the old constraint foo>=3.5 and the new version 4.7.2,
the constraint would be updated to foo>=4.7.
The PYPROJECT argument should point to a pyproject.toml file,
or to a directory containing such a file.
If this argument is not specified,
the one in the current working directory will be used.
Options:
--lockfile PATHWhere to load versions from. Inferred if possible.- file: use the path as the lockfile
- directory: use the lockfile in that directory
- default: use the lockfile in the
PYPROJECTdirectory
--backup PATHStore a backup in this file.--name FILTERInclude/exclude constraints to edit by package name. [default: edit all]--helpShow this help message and exit.
Examples
These examples demonstrate the command line interface. Further below are examples of how different version constraint operators are bumped.
Uv Example
Ganzua will edit standard/uv pyproject.toml files.
Given an example directory with a pyproject.toml file with outdated constraints:
$ cp $CORPUS/constraints-uv-pyproject.toml $EXAMPLE/pyproject.toml
And given an uv.lock file with the following contents:
| name | version |
|---|---|
| annotated-types | 0.7.0 |
| example | 0.2.0 |
| typing-extensions | 4.14.1 |
When we bump the constraints (and make a backup):
$ ganzua constraints bump $EXAMPLE --backup=$EXAMPLE/backup-pyproject.toml
Then the pyproject.toml file contains updated constraints,
whereas the backup contains the old version.
$ cat $EXAMPLE/pyproject.toml$ cat $EXAMPLE/backup-pyproject.toml$ cat $CORPUS/constraints-uv-pyproject.toml
output for the above commands
Output for:
$ cat $EXAMPLE/pyproject.toml
[project]
name = "example"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"Typing.Extensions>=4,<5", # moar type annotations
"merrily-ignored",
[42, "also ignored"], # we ignore invalid junk
]
[project.optional-dependencies]
extra1 = [
"annotated-types>=0.7.0,==0.7.*",
]
extra2 = false # known invalid
extra3 = ["ndr"]
[dependency-groups]
group-a = ["typing-extensions~=4.14"]
group-b = [{include-group = "group-a"}, "annotated-types~=0.7.0"]
Output for:
$ cat $EXAMPLE/backup-pyproject.toml$ cat $CORPUS/constraints-uv-pyproject.toml
[project]
name = "example"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"Typing.Extensions>=3,<4", # moar type annotations
"merrily-ignored",
[42, "also ignored"], # we ignore invalid junk
]
[project.optional-dependencies]
extra1 = [
"annotated-types >=0.6.1, ==0.6.*",
]
extra2 = false # known invalid
extra3 = ["ndr"]
[dependency-groups]
group-a = ["typing-extensions ~=3.4"]
group-b = [{include-group = "group-a"}, "annotated-types ~=0.6.1"]
Poetry example
We can also do the same with Poetry pyproject.toml files.
Given an example directory with a pyproject.toml file with outdated constraints:
$ cp corpus/constraints-poetry-pyproject.toml $EXAMPLE/pyproject.toml
And given a poetry.lock file with the following content:
| name | version |
|---|---|
| annotated-types | 0.7.0 |
| example | 0.2.0 |
| typing-extensions | 4.14.1 |
When we bump the constraints:
$ ganzua constraints bump $EXAMPLE
Then the pyproject.toml file contains updated constraints.
$ cat $EXAMPLE/pyproject.toml
[tool.poetry.dependencies]
Typing_Extensions = "^4.14"
ignored-garbage = { not-a-version = true }
[build-system]
[tool.poetry.group.poetry-a.dependencies]
typing-extensions = { version = "^4.14" }
already-unconstrained = "*"
No-op example
When the constraints are already up to date, Ganzua doesn't modify them.
Given a pyproject.toml file:
$ cp corpus/new-uv-project/pyproject.toml $EXAMPLE/pyproject.toml
When we bump constraints using an already-matching lockfile:
$ ganzua constraints bump --lockfile=corpus/new-uv-project/uv.lock $EXAMPLE/pyproject.toml
Then the old and new file are the same.
$ cat corpus/new-uv-project/pyproject.toml$ cat $EXAMPLE/pyproject.toml
output for the above commands
[project]
name = "example"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"annotated-types>=0.7.0",
"typing-extensions>=4",
]
Empty example
Since Ganzua does not validate that the pyproject.toml file is semantically valid,
it's also possible to edit an empty file.
Given an empty pyproject.toml file:
$ touch $EXAMPLE/pyproject.toml
When we bump the constraints in the empty pyproject.toml file,
then Ganzua succeeds,
and the pyproject.toml file remains empty:
$ ganzua constraints bump --lockfile=corpus/new-uv-project/uv.lock $EXAMPLE/pyproject.toml
$ cat $EXAMPLE/pyproject.toml
Split versions
If a project has split versions for a package, matching requirements will be skipped.
It is not possible to determine which requirement is supposed to be matched up with which locked version, so these instances will have to be fixed manually.
Note that depending on workflow, this means the lockfile and the pyproject.toml might become inconsistent.
The exact behavior here is not considered stable. Future Ganzua versions may change the handling of split versions if solutions to these problems are found.
Let's look at a concrete example with a split version:
$ cp $CORPUS/split/old.pyproject.toml $EXAMPLE/pyproject.toml
$ cp $CORPUS/split/uv.lock $EXAMPLE/uv.lock
Current pyproject.toml file:
$ cat $EXAMPLE/pyproject.toml
[project]
name = "split"
version = "0.1.0"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"typing_extensions >=4.7.1 ; python_version >= '3.14'",
"typing_extensions >=3.7.2,<4 ; python_version < '3.14'",
]
Currently locked versions:
| package | version |
|---|---|
| split | 0.1.0 |
| typing-extensions | 3.10.0.2 |
| typing-extensions | 4.15.0 |
Now we try to bump the old constraints:
$ ganzua constraints bump $EXAMPLE
ganzua: package `typing-extensions` has multiple candidate versions: 3.10.0.2, 4.15.0
But all we get is a warning. The constraints remain unchanged:
$ cat $EXAMPLE/pyproject.toml
[project]
name = "split"
version = "0.1.0"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"typing_extensions >=4.7.1 ; python_version >= '3.14'",
"typing_extensions >=3.7.2,<4 ; python_version < '3.14'",
]
Default files example
Ganzua tries to infer the pyproject.toml and lockfile.
Empty directory:
Running Ganzua against an empty directory will fail, as it cannot locate a pyproject.toml file in there.
$ env -C $EXAMPLE ganzua constraints bump --lockfile=$CORPUS/new-uv-project/uv.lock
Usage: ganzua constraints bump [OPTIONS] [PYPROJECT]
Try 'ganzua constraints bump --help' for help.
Error: Did not find default `pyproject.toml`.
[command exited with status 2]
Infering pyproject.toml:
If no pyproject.toml file is specified explicitly, Ganzua will look for it in the current working directory.
If a path to a directory is given, the file in that directory will be used.
For this example, let's create a pyproject.toml file:
$ cp $CORPUS/old-uv-project/pyproject.toml $EXAMPLE/pyproject.toml
$ ls $EXAMPLE
pyproject.toml
Now, all of the following commands are equivalent:
$ env -C $EXAMPLE ganzua constraints bump --lockfile=$CORPUS/new-uv-project/uv.lock
$ env -C $EXAMPLE ganzua constraints bump --lockfile=$CORPUS/new-uv-project/uv.lock pyproject.toml
$ env -C $EXAMPLE ganzua constraints bump --lockfile=$CORPUS/new-uv-project/uv.lock .
$ ganzua constraints bump --lockfile=$CORPUS/new-uv-project/uv.lock $EXAMPLE/pyproject.toml
$ ganzua constraints bump --lockfile=$CORPUS/new-uv-project/uv.lock $EXAMPLE
Lockfile is required:
If we leave out the explicit --lockfile argument, then Ganzua will fail, because there's currently no lockfile in the example directory:
$ ls $EXAMPLE
pyproject.toml
$ env -C $EXAMPLE ganzua constraints bump
Usage: ganzua constraints bump [OPTIONS] [PYPROJECT]
Try 'ganzua constraints bump --help' for help.
Error: Could not infer `--lockfile` for `.`.
[command exited with status 2]
But an explicit lockfile succeeds:
$ env -C $EXAMPLE ganzua constraints bump --lockfile=$CORPUS/new-uv-project/uv.lock
Infering lockfiles:
If no explicit --lockfile is given, then Ganzua will look for an uv.lock or poetry.lock file in the directory of the pyproject.toml file.
The lockfile may also be given as a different directory, in which case Ganzua will look for the lockfile in that directory.
If we add a lockfile in the example directory, then we can leave out the lockfile.
$ cp $CORPUS/new-uv-project/uv.lock $EXAMPLE/uv.lock
$ ls $EXAMPLE
pyproject.toml
uv.lock
So now these commands are equivalent:
$ ganzua constraints bump $EXAMPLE
$ ganzua constraints bump --lockfile=$EXAMPLE $EXAMPLE
$ ganzua constraints bump --lockfile=$EXAMPLE/uv.lock $EXAMPLE
$ env -C $EXAMPLE ganzua constraints bump
$ env -C $EXAMPLE ganzua constraints bump --lockfile=./
$ env -C $EXAMPLE ganzua constraints bump --lockfile=uv.lock
Conflicting lockfiles: If there are multiple candidate lockfiles in the directory, then Ganzua will report a conflict, unless a full path to a specific lockfile is given:
$ touch $EXAMPLE/poetry.lock
$ ls $EXAMPLE
poetry.lock
pyproject.toml
uv.lock
$ ganzua constraints bump $EXAMPLE
Usage: ganzua constraints bump [OPTIONS] [PYPROJECT]
Try 'ganzua constraints bump --help' for help.
Error: Could not infer `--lockfile` for `${EXAMPLE}`.
Note: Candidate lockfile: ${EXAMPLE}/uv.lock
Note: Candidate lockfile: ${EXAMPLE}/poetry.lock
[command exited with status 2]
$ ganzua constraints bump $EXAMPLE --lockfile=$EXAMPLE/uv.lock
Filters
Can use filters to only bump certain constraints. Read the filter manual for further details on how filters work.
Added in Ganzua 0.4.0: --name filters.
Let's recall the above uv example.
$ cp $CORPUS/constraints-uv-pyproject.toml $EXAMPLE/pyproject.toml
| name | version |
|---|---|
| annotated-types | 0.7.0 |
| example | 0.2.0 |
| typing-extensions | 4.14.1 |
We can now decide that we only want to bump the typing-extensions constraints, not the annotated-types constraints:
$ ganzua constraints bump $EXAMPLE --name=typing-extensions --backup=$EXAMPLE/old.toml
Old constraints:
$ ganzua constraints inspect $EXAMPLE/old.toml --format=markdown
| package | version | group/extra |
|-------------------|-----------------|----------------------------------|
| annotated-types | ==0.6.*,>=0.6.1 | extra `extra1` |
| annotated-types | ~=0.6.1 | group `group-b` |
| merrily-ignored | | |
| ndr | | extra `extra3` |
| typing-extensions | <4,>=3 | |
| typing-extensions | ~=3.4 | group `group-a`, group `group-b` |
Updated constraints:
$ ganzua constraints inspect $EXAMPLE --format=markdown
| package | version | group/extra |
|-------------------|-----------------|----------------------------------|
| annotated-types | ==0.6.*,>=0.6.1 | extra `extra1` |
| annotated-types | ~=0.6.1 | group `group-b` |
| merrily-ignored | | |
| ndr | | extra `extra3` |
| typing-extensions | <5,>=4 | |
| typing-extensions | ~=4.14 | group `group-a`, group `group-b` |
Examples of different constraints
The following examples illustrate how version bumping works.
The different version specifier operators are defined in PEP-440, with the up-to-date specifications on packaging.python.org.
~=1.2.3compatible release examples==1.2.3strict version matching examples==1.*prefix version matching examples!=1.2.3version exclusion examples>=1.2.3lower version bound examples<=1.2.3,>1.2.3,<1.2.3ordered comparison examples===1.2.3+build123arbitrary equality examples
Certain combinations of operators are often used to describe SemVer constraints,
e.g. >=1.2.3,<2 or >=1.2.3,==1.* (see SemVer examples).
Poetry supports certain additional operators:
1.2.3strict version matching examples1.*prefix version matching examples~1.2.3Poetry tilde examples^1.2.3SemVer examples
Syntax for example tables:
- The
lockedcolumn shows the locked version in format<package> <version>, e.g.foo 1.2.3. - The
oldandnewcolumn show the constraint before and after bumping.- may use PEP-508/PEP-440 expressions, e.g.
foo >=1.2.3 - may describe Poetry version constraints using a
<package> = <version>format but without quoting that would appear in a TOML file, e.g.foo = *
- may use PEP-508/PEP-440 expressions, e.g.
Basic examples
Will only bump packages that are present in the lockfile. Other constraints are not affected:
| locked | old | new |
|---|---|---|
locked 1.2.3 | other >=4,<5 | other >=4,<5 |
locked 1.2.3 | other = ^4 | other = ^4 |
Bumping matches the granularity of the bounds that are currently present.
If there are no constraints for that package, bumping will not add any bounds:
| locked | old | new |
|---|---|---|
package 1.2.3 | package | package |
package 1.2.3 | package = * | package = * |
Lower version bound examples
When the major version changes, a >= bound is bumped with matching granularity:
| locked | old | new |
|---|---|---|
major 7.1.2 | major>=4 | major>=7 |
minor 7.1.2 | minor>=4.3 | minor>=7.1 |
patch 7.1.2 | patch>=4.3.2 | patch>=7.1.2 |
minor-poetry 7.1.2 | minor-poetry = >=4.3 | minor-poetry = >=7.1 |
When the minor or patch version changes, a >= bound is bumped with matching granularity.
Here, no changes to the major constraint are needed.
Granularity is determined based on how many version elements have been explicitly provided.
So whereas the versions 4 and 4.0 are semantically equivalent, they have different granularity.
| locked | old | new |
|---|---|---|
major 4.5.6 | major>=4 | major>=4 |
minor0 4.5.6 | minor0>=4.0 | minor0>=4.5 |
minor 4.5.6 | minor>=4.3 | minor>=4.5 |
patch 4.5.6 | patch>=4.3.2 | patch>=4.5.6 |
minor-poetry 4.5.6 | minor-poetry = >=4.3 | minor-poetry = >=4.5 |
patch-poetry 4.5.6 | patch-poetry = >=4.3.2 | patch-poetry = >=4.5.6 |
Constraints may also be downgraded if this is necessary to match the locked version:
| locked | old | new |
|---|---|---|
major 4.5.6 | major>=7 | major>=4 |
minor 4.5.6 | minor>=4.9 | minor>=4.5 |
patch 4.5.6 | patch>=4.5.9 | patch>=4.5.6 |
SemVer examples
Poetry has the ^ SemVer operator.
It is bumped the same way as >= bounds:
| locked | old | new |
|---|---|---|
major 7.1.2 | major = ^4 | major = ^7 |
minor 7.1.2 | minor = ^7.0 | minor = ^7.1 |
patch 7.1.2 | patch = ^7.1.1 | patch = ^7.1.2 |
downgrade 7.1.2 | downgrade = ^9.8 | downgrade = ^7.1 |
However, SemVer bounds are also frequently expressed via PEP-440 expressions.
For example, the Poetry bound ^4.1 can also be expressed via the following idioms:
- upper bounds
>=4.1,<5 - version matching prefixes
>=4.1,==4.*
Upper bounds will be bumped when they look like a SemVer bound (>=N, <N+1) and would be outdated by the new version:
| locked | old | new |
|---|---|---|
major 7.1.2 | major>=4,<5 | major>=7,<8 |
minor0 7.1.2 | minor0>=4.0,<5 | minor0>=7.1,<8 |
minor 7.1.2 | minor>=4.9,<5 | minor>=7.1,<8 |
patch 7.1.2 | patch>=4.0.1,<5 | patch>=7.1.2,<8 |
However, upper bounds are not changed if they remain valid for the locked version:
| locked | old | new |
|---|---|---|
minor-upgrade 4.5.6 | minor-upgrade>=4.3,<5 | minor-upgrade>=4.5,<5 |
minor-downgrade 4.5.6 | minor-downgrade>=4.9,<5 | minor-downgrade>=4.5,<5 |
wide 7.1.2 | wide>=4.5,<9 | wide>=7.1,<9 |
The prefix version matching SemVer idiom behaves more intuitively because each bound in the constraint will be bumped independently to match the new version:
| locked | old | new |
|---|---|---|
major 7.1.2 | major>=4,==4.* | major>=7,==7.* |
minor0 7.1.2 | minor0>=4.0,==4.* | minor0>=7.1,==7.* |
minor 7.1.2 | minor>=4.9,==4.* | minor>=7.1,==7.* |
minor-match 7.1.2 | minor-match>=4.9,==4.9.* | minor-match>=7.1,==7.1.* |
patch 7.1.2 | patch>=4.0.1,==4.* | patch>=7.1.2,==7.* |
downgrade 7.1.2 | downgrade >=9.8,==9.* | downgrade>=7.1,==7.* |
minor-upgrade 4.5.6 | minor-upgrade>=4.3,==4.* | minor-upgrade>=4.5,==4.* |
minor-downgrade 4.5.6 | minor-downgrade>=4.9,==4.* | minor-downgrade>=4.5,==4.* |
Strict version matching examples
The == operator can be used for exact version matching (without * wildcards for prefix matching).
Exact bounds will be bumped to match the locked version exactly.
The == operator is also implied when a Poetry version constraint doesn't use any operators.
expand PEP-440 examples
| locked | old | new |
|---|---|---|
foo 7.1.2 | foo==4.3.2 | foo==7.1.2 |
foo 7.1.2 | foo==4.3 | foo==7.1.2 |
foo 7.1.2 | foo==4 | foo==7.1.2 |
foo 7.1.2 | foo==7 | foo==7.1.2 |
foo 7.1.2 | foo==7.1 | foo==7.1.2 |
foo 7.1.2 | foo==7.1.2 | foo==7.1.2 |
expand Poetry examples
| locked | old | new |
|---|---|---|
foo 7.1.2 | foo = 4.3.2 | foo = 7.1.2 |
foo 7.1.2 | foo = 4.3 | foo = 7.1.2 |
foo 7.1.2 | foo = 4 | foo = 7.1.2 |
foo 7.1.2 | foo = 7 | foo = 7.1.2 |
foo 7.1.2 | foo = 7.1 | foo = 7.1.2 |
foo 7.1.2 | foo = 7.1.2 | foo = 7.1.2 |
Prefix version matching examples
When the == operator is combined with * wildcards, it performs prefix matching.
Such constraints are updated with matching granularity.
The == operator is implied when a Poetry version constraint doesn't use any operators.
expand PEP-440 examples
| locked | old | new |
|---|---|---|
foo 7.1.2 | foo==4.3.* | foo==7.1.* |
foo 7.1.2 | foo==4.* | foo==7.* |
foo 7.1.2 | foo==7.* | foo==7.* |
foo 7.1.2 | foo==7.0.* | foo==7.1.* |
foo 7.1.2 | foo==7.1.* | foo==7.1.* |
expand Poetry examples
| locked | old | new |
|---|---|---|
foo 7.1.2 | foo = 4.3.* | foo = 7.1.* |
foo 7.1.2 | foo = 4.* | foo = 7.* |
foo 7.1.2 | foo = 7.* | foo = 7.* |
foo 7.1.2 | foo = 7.0.* | foo = 7.1.* |
foo 7.1.2 | foo = 7.1.* | foo = 7.1.* |
Compatible release examples
The ~= compatible release operator (spec) is bumped the same way as >= lower bounds:
it will be bumped so that it is a lower bound for the currently locked version, matching granularity.
Note that a ~= compatible release clause needs to consist at least of ~=major.minor.
A single element is insufficient.
See also the Poetry ~ tilde operator.
expand PEP-440 examples
| locked | old | new |
|---|---|---|
major 7.1.2 | major~=4.5 | major~=7.1 |
minor0 4.5.6 | minor0~=4.0 | minor0~=4.5 |
minor 4.5.6 | minor~=4.3 | minor~=4.5 |
patch 4.5.6 | patch~=4.3.2 | patch~=4.5.6 |
minor-poetry 4.5.6 | minor-poetry = ~=4.3 | minor-poetry = ~=4.5 |
patch-poetry 4.5.6 | patch-poetry = ~=4.3.2 | patch-poetry = ~=4.5.6 |
major-downgrade 4.5.6 | major-downgrade~=7.9 | major-downgrade~=4.5 |
minor-downgrade 4.5.6 | minor-downgrade~=4.9 | minor-downgrade~=4.5 |
patch-downgrade 4.5.6 | patch-downgrade~=4.5.9 | patch-downgrade~=4.5.6 |
noop 1.2.3 | noop~=1.2.3 | noop~=1.2.3 |
Poetry tilde examples
Tilde requirements (spec) can only occur within the [tool.poetry] table.
They work similarly to compatible release constraints,
but describe different version ranges.
Notably, a single segment like ~1 is allowed.
Poetry bumps such constraints the same way as >= lower version bounds.
expand Poetry examples
| locked | old | new |
|---|---|---|
major 7.1.2 | major = ~4 | major = ~7 |
major 7.1.2 | major = ~4.5 | major = ~7.1 |
minor0 4.5.6 | minor0 = ~4.0 | minor0 = ~4.5 |
minor 4.5.6 | minor = ~4.3 | minor = ~4.5 |
patch 4.5.6 | patch = ~4.3.2 | patch = ~4.5.6 |
major-downgrade 4.5.6 | major-downgrade = ~7 | major-downgrade = ~4 |
major-downgrade 4.5.6 | major-downgrade = ~7.9 | major-downgrade = ~4.5 |
minor-downgrade 4.5.6 | minor-downgrade = ~4.9 | minor-downgrade = ~4.5 |
patch-downgrade 4.5.6 | patch-downgrade = ~4.5.9 | patch-downgrade = ~4.5.6 |
noop 1.2.3 | noop = ~1.2.3 | noop = ~1.2.3 |
noop 1.2.3 | noop = ~1.2 | noop = ~1.2 |
noop 1.2.3 | noop = ~1 | noop = ~1 |
Version exclusion examples
The != version exclusion clause (spec) is the inverse of == version matching.
It can be used to exclude specific versions like !=1.2.3 or prefixes like !=1.2.*.
When Ganzua bumps a version exclusion specifier, the specifier will be retained as-is, unless it would exclude the currently locked version.
exclusions are kept by default
| locked | old | new |
|---|---|---|
foo 7.1.2 | foo!=4.3.2 | foo!=4.3.2 |
foo 7.1.2 | foo!=4.* | foo!=4.* |
foo 7.1.2 | foo!=7.9.* | foo!=7.9.* |
foo 7.1.2 | foo!=7.0.1 | foo!=7.0.1 |
exclusions are removed if they would exclude the current version
| locked | old | new |
|---|---|---|
foo 7.1.2 | foo!=7.* | foo |
foo 7.1.2 | foo!=7.1.* | foo |
foo 7.1.2 | foo!=7.1.2 | foo |
Arbitrary equality examples
The === arbitrary equality comparison (spec) matches only an exact version, without any semantic interpretation of the version number.
When Ganzua bumps arbitrary equality clauses, it always sets the currently locked version. Ganzua requires that the currently locked version is valid, but allows the old version constraint to contain an invalid version number.
expand examples
| locked | old | new |
|---|---|---|
foo 7.1.2.post1+abc | foo===7.1.2 | foo===7.1.2.post1+abc |
foo 7.1.2 | foo===7.1.2.post1+abc | foo===7.1.2 |
foo 7.1.2 | foo===whatever | foo===7.1.2 |
unmodified 7.1.2 | unmodified===7.1.2 | unmodified===7.1.2 |
Ordered comparison examples
The version constraint operators <=, <, and > (spec) cannot be bumped meaningfully.
Ganzua will retain them as-is if they allow the locked version, else remove them.
expand examples
| locked | old | new |
|---|---|---|
foo 7.1.2 | foo>4 | foo>4 |
foo 7.1.2 | foo>8 | foo |
foo 7.1.2 | foo<5 | foo |
foo 7.1.2 | foo<8 | foo<8 |
foo 7.1.2 | foo<=5 | foo |
foo 7.1.2 | foo<=8 | foo<=8 |
foo 7.1.2 | foo<=7.1 | foo |
foo 7.1.2 | foo<=7.2 | foo<=7.2 |