Skip to content

Version

feu.version

Contain functions to manage package versions.

feu.version.PackageBounds dataclass

Lower and upper version bounds for a package dependency.

Attributes:

Name Type Description
name str

The canonical package name as declared in the dependency specifier.

lower str | None

The lower bound version string (from >= or > specifiers), or None if no lower bound is declared.

upper str | None

The upper bound version string (from < or <= specifiers), or None if no upper bound is declared.

section str

The pyproject.toml section where the dependency was found (e.g. 'project.dependencies', 'project.optional-dependencies.dev', 'dependency-groups.dev').

feu.version.compare_version

compare_version(
    package: str, op: Callable, version: str
) -> bool

Compare a package version to a given version.

Parameters:

Name Type Description Default
package str

The package to check.

required
op Callable

The comparison operator.

required
version str

The version to compare with.

required

Returns:

Type Description
bool

The comparison status.

Example
>>> import operator
>>> from feu.version import compare_version
>>> compare_version("pytest", op=operator.ge, version="7.3.0")
True

feu.version.fetch_latest_major_versions

fetch_latest_major_versions(
    package: str,
    lower: str | None = None,
    upper: str | None = None,
    include_lower_bound: bool = False,
) -> tuple[str, ...]

Get the latest version for each major version for a given package.

Parameters:

Name Type Description Default
package str

The package name.

required
lower str | None

The lower version bound (inclusive). If None, no lower limit is applied.

None
upper str | None

The upper version bound (exclusive). If None, no upper limit is applied.

None
include_lower_bound bool

If True and lower is not None, the first stable version at or above lower is included in the result. This ensures the lower bound itself is always represented, even if it is not the latest patch for its major release. If lower is None, this argument has no effect. Defaults to False.

False

Returns:

Type Description
tuple[str, ...]

A tuple containing the latest version for each major version, sorted by major version number. If include_lower_bound is True and lower is not None, the first stable version at or above lower is also included.

Example
>>> from feu.version import fetch_latest_major_versions
>>> versions = fetch_latest_major_versions("requests", lower="2.0")  # doctest: +SKIP

feu.version.fetch_latest_major_versions_map

fetch_latest_major_versions_map(
    packages: Sequence[PackageBounds],
    include_lower_bound: bool = False,
) -> dict[str, list[str]]

Fetch the latest major versions for a sequence of packages.

For each PackageBounds in packages, calls fetch_latest_major_versions with the package name and lower bound, and collects the results into a dictionary.

If a package appears more than once in packages (e.g. because it was found in multiple sections), the last entry wins.

Parameters:

Name Type Description Default
packages Sequence[PackageBounds]

A sequence of PackageBounds instances, typically obtained from read_pyproject_dependencies or read_pyproject_optional_dependencies.

required
include_lower_bound bool

If True, the first stable version at or above each package's lower bound is included in its version list. Has no effect for packages whose lower bound is None. Defaults to False.

False

Returns:

Type Description
dict[str, list[str]]

A dictionary mapping each package name to the list of latest major version strings returned by fetch_latest_major_versions.

Example
>>> from feu.version import fetch_latest_major_versions_map, read_pyproject_dependencies
>>> bounds = read_pyproject_dependencies("pyproject.toml")  # doctest: +SKIP
>>> versions = fetch_latest_major_versions_map(bounds)  # doctest: +SKIP

feu.version.fetch_latest_minor_versions

fetch_latest_minor_versions(
    package: str,
    lower: str | None = None,
    upper: str | None = None,
    include_lower_bound: bool = False,
) -> tuple[str, ...]

Get the latest version for each minor version for a given package.

Parameters:

Name Type Description Default
package str

The package name.

required
lower str | None

The lower version bound (inclusive). If None, no lower limit is applied.

None
upper str | None

The upper version bound (exclusive). If None, no upper limit is applied.

None
include_lower_bound bool

If True and lower is not None, the first stable version at or above lower is included in the result. This ensures the lower bound itself is always represented, even if it is not the latest patch for its minor release. If lower is None, this argument has no effect. Defaults to False.

False

Returns:

Type Description
tuple[str, ...]

A tuple containing the latest version for each minor version, sorted by minor version number. If include_lower_bound is True and lower is not None, the first stable version at or above lower is also included.

Example
>>> from feu.version import fetch_latest_minor_versions
>>> versions = fetch_latest_minor_versions("requests", lower="2.28")  # doctest: +SKIP

feu.version.fetch_latest_minor_versions_map

fetch_latest_minor_versions_map(
    packages: Sequence[PackageBounds],
    include_lower_bound: bool = False,
) -> dict[str, list[str]]

Fetch the latest minor versions for a sequence of packages.

For each PackageBounds in packages, calls fetch_latest_minor_versions with the package name and lower bound, and collects the results into a dictionary.

If a package appears more than once in packages (e.g. because it was found in multiple sections), the last entry wins.

Parameters:

Name Type Description Default
packages Sequence[PackageBounds]

A sequence of PackageBounds instances, typically obtained from read_pyproject_dependencies or read_pyproject_optional_dependencies.

required
include_lower_bound bool

If True, the first stable version at or above each package's lower bound is included in its version list. Has no effect for packages whose lower bound is None. Defaults to False.

False

Returns:

Type Description
dict[str, list[str]]

A dictionary mapping each package name to the list of latest minor version strings returned by fetch_latest_minor_versions.

Example
>>> from feu.version import fetch_latest_minor_versions_map, read_pyproject_dependencies
>>> bounds = read_pyproject_dependencies("pyproject.toml")  # doctest: +SKIP
>>> versions = fetch_latest_minor_versions_map(bounds)  # doctest: +SKIP
>>> versions_with_lower = fetch_latest_minor_versions_map(
...     bounds, include_lower_bound=True
... )  # doctest: +SKIP

feu.version.fetch_latest_stable_version

fetch_latest_stable_version(package: str) -> str

Get the latest stable valid versions for a given package.

Parameters:

Name Type Description Default
package str

The package name.

required

Returns:

Type Description
str

The latest stable valid versions.

Example
>>> from feu.version import fetch_latest_stable_version
>>> version = fetch_latest_stable_version("requests")  # doctest: +SKIP

feu.version.fetch_latest_version

fetch_latest_version(package: str) -> str

Get the latest valid versions for a given package.

Parameters:

Name Type Description Default
package str

The package name.

required

Returns:

Type Description
str

The latest valid versions.

Example
>>> from feu.version import fetch_latest_version
>>> version = fetch_latest_version("requests")  # doctest: +SKIP

feu.version.fetch_pypi_versions cached

fetch_pypi_versions(
    package: str, reverse: bool = False
) -> tuple[str, ...]

Get the package versions available on PyPI.

The package versions are read from PyPI.

Parameters:

Name Type Description Default
package str

The package name.

required
reverse bool

If False, sort in ascending order; if True, sort in descending order.

False

Returns:

Type Description
tuple[str, ...]

A list containing the sorted version strings.

Example
>>> from feu.version import fetch_pypi_versions
>>> versions = fetch_pypi_versions("requests")  # doctest: +SKIP

feu.version.fetch_sampled_latest_minor_versions

fetch_sampled_latest_minor_versions(
    package: str,
    lower: str | None = None,
    upper: str | None = None,
    n: int = 1,
    include_lower_bound: bool = False,
) -> tuple[str, ...]

Get a sampled subset of the latest minor versions for a given package.

Fetches the latest version for each minor release, then returns every n-th version plus the most recent one, deduplicated and sorted. This is useful for generating a representative but not exhaustive set of versions to test against.

Parameters:

Name Type Description Default
package str

The package name.

required
lower str | None

The lower version bound (inclusive). If None, no lower limit is applied.

None
upper str | None

The upper version bound (exclusive). If None, no upper limit is applied.

None
n int

The sampling stride. Every n-th version is kept, along with the most recent version. Defaults to 1, which keeps all versions.

1
include_lower_bound bool

If True and lower is not None, the first stable version at or above lower is included in the result alongside the sampled versions. Has no effect if lower is None. Defaults to False.

False

Returns:

Type Description
tuple[str, ...]

A sorted tuple of sampled version strings.

Example
>>> from feu.version import fetch_sampled_latest_minor_versions
>>> versions = fetch_sampled_latest_minor_versions(
...     "requests", lower="2.28", n=2
... )  # doctest: +SKIP

feu.version.fetch_versions

fetch_versions(
    package: str,
    lower: str | None = None,
    upper: str | None = None,
) -> tuple[str, ...]

Get the valid versions for a given package.

Parameters:

Name Type Description Default
package str

The package name.

required
lower str | None

The lower version bound (inclusive). If None, no lower limit is applied.

None
upper str | None

The upper version bound (exclusive). If None, no upper limit is applied.

None

Returns:

Type Description
tuple[str, ...]

A tuple containing the valid versions.

Example
>>> from feu.version import fetch_versions
>>> versions = fetch_versions("requests")  # doctest: +SKIP

feu.version.filter_every_n_versions

filter_every_n_versions(
    versions: Sequence[str], n: int
) -> list[str]

Filter a list of version strings, keeping only every n-th version using 0-based indexing.

This function preserves the original order of the input list and returns a new list containing only the versions at positions that are multiples of n (using 0-based indexing). For example, if n = 2, the function keeps the 0th, 2nd, 4th, ... versions from the list.

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings.

required
n int

The interval for selecting versions. Must be >= 1.

required

Returns:

Type Description
list[str]

A new list containing only every n-th version in versions, starting from index 0.

Raises:

Type Description
ValueError

If n is less than 1.

Example
>>> from feu.version import filter_every_n_versions
>>> versions = filter_every_n_versions(["1.0", "1.1", "1.2", "1.3", "1.5", "1.6"], n=2)
>>> versions
['1.0', '1.2', '1.5']
>>> versions = filter_every_n_versions(["1.0", "1.1", "1.2", "1.3", "1.5", "1.6"], n=1)
>>> versions
['1.0', '1.1', '1.2', '1.3', '1.5', '1.6']

feu.version.filter_last_n_versions

filter_last_n_versions(
    versions: Sequence[str], n: int
) -> list[str]

Return only the last n versions from a list of version strings.

This function preserves the original ordering of the final n elements. If n is greater than the number of versions available, the entire list is returned.

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings.

required
n int

Number of versions to keep from the end of the list. Must be > 0.

required

Returns:

Type Description
list[str]

A new list containing only the last n versions, in order.

Raises:

Type Description
ValueError

If n is less than or equal to 0.

Example
>>> from feu.version import filter_last_n_versions
>>> versions = filter_last_n_versions(["1.0", "1.1", "1.2", "1.3"], n=2)
>>> versions
['1.2', '1.3']
>>> versions = filter_last_n_versions(["1.0", "1.1", "1.2", "1.3"], n=5)
>>> versions
['1.0', '1.1', '1.2', '1.3']

feu.version.filter_range_versions

filter_range_versions(
    versions: Sequence[str],
    lower: str | None = None,
    upper: str | None = None,
) -> list[str]

Filter a list of version strings to include only versions within optional bounds.

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings.

required
lower str | None

The lower version bound (inclusive). If None, no lower limit is applied.

None
upper str | None

The upper version bound (exclusive). If None, no upper limit is applied.

None

Returns:

Type Description
list[str]

A list of version strings that fall within the specified bounds.

Example
>>> from feu.version import filter_range_versions
>>> versions = filter_range_versions(
...     ["1.0.0", "1.2.0", "1.3.0", "2.0.0"], lower="1.1.0", upper="2.0.0"
... )
>>> versions
['1.2.0', '1.3.0']
>>> versions = filter_range_versions(["0.9.0", "1.0.0", "1.1.0"], lower="1.0.0")
>>> versions
['1.0.0', '1.1.0']

feu.version.filter_stable_versions

filter_stable_versions(
    versions: Sequence[str],
) -> list[str]

Filter out pre-release, post-release, and dev-release versions from a list of version strings.

A stable version is defined as
  • Not a pre-release (e.g., alpha a, beta b, release candidate rc)
  • Not a post-release (e.g., 1.0.0.post1)
  • Not a development release (e.g., 1.0.0.dev1)

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings.

required

Returns:

Type Description
list[str]

A list containing only stable version strings.

Example
>>> from feu.version import filter_stable_versions
>>> versions = filter_stable_versions(
...     ["1.0.0", "1.0.0a1", "2.0.0", "2.0.0.dev1", "3.0.0.post1"]
... )
>>> versions
['1.0.0', '2.0.0']

feu.version.filter_valid_versions

filter_valid_versions(versions: Sequence[str]) -> list[str]

Filter out invalid version strings based on PEP 440.

A valid version is one that can be parsed by packaging.version.Version. Invalid versions include strings that don't conform to semantic versioning rules.

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings.

required

Returns:

Type Description
list[str]

A list containing only valid version strings.

Example
>>> from feu.version import filter_valid_versions
>>> versions = filter_valid_versions(
...     [
...         "1.0.0",
...         "1.0.0a1",
...         "2.0.0.post1",
...         "not-a-version",
...         "",
...         "2",
...         "3.0",
...         "v1.0.0",
...         "1.0.0.0.0",
...         "4.0.0.dev1",
...     ]
... )
>>> versions
['1.0.0', '1.0.0a1', '2.0.0.post1', '2', '3.0', 'v1.0.0', '1.0.0.0.0', '4.0.0.dev1']

feu.version.get_package_bounds

get_package_bounds(
    packages: Sequence[PackageBounds], name: str
) -> PackageBounds

Return the first PackageBounds matching a given package name.

The name comparison is case-insensitive and treats hyphens and underscores as equivalent, following PEP 508 normalisation rules.

Parameters:

Name Type Description Default
packages Sequence[PackageBounds]

A sequence of PackageBounds instances to search.

required
name str

The package name to look up.

required

Returns:

Type Description
PackageBounds

The first PackageBounds whose name matches name.

Raises:

Type Description
ValueError

If no entry matching name is found in packages.

Example
>>> from feu.version import PackageBounds, get_package_bounds
>>> packages = [
...     PackageBounds(
...         name="numpy", lower="1.21", upper="2.0", section="project.dependencies"
...     ),
...     PackageBounds(
...         name="torch", lower="2.0", upper=None, section="project.dependencies"
...     ),
... ]
>>> get_package_bounds(packages, "numpy")
PackageBounds(name='numpy', lower='1.21', upper='2.0', section='project.dependencies')

feu.version.get_package_version

get_package_version(package: str) -> Version | None

Get the package version.

Parameters:

Name Type Description Default
package str

The package name.

required

Returns:

Type Description
Version | None

The package version.

Example
>>> from feu.version import get_package_version
>>> get_package_version("pytest")
<Version('...')>

feu.version.get_python_major_minor cached

get_python_major_minor() -> str

Get the MAJOR.MINOR version of the current python.

Returns:

Type Description
str

The MAJOR.MINOR version of the current python.

Example
>>> from feu.version import get_python_major_minor
>>> get_python_major_minor()  # doctest: +SKIP

feu.version.latest_major_versions

latest_major_versions(versions: Sequence[str]) -> list[str]

Return the latest version for each major version in a list of semantic versions.

This function takes a list of semantic version strings (e.g. "1.0.0", "1.2.1", "2.0.0"), groups them by their major version number, and returns only the latest version from each major group (based on minor and patch numbers).

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings in semantic version format.

required

Returns:

Type Description
list[str]

A list containing the latest version for each major version, sorted by major version number.

Example
>>> from feu.version import latest_major_versions
>>> versions = latest_major_versions(["1.0.0", "1.1.0", "1.2.0", "1.2.1", "2.0.0"])
>>> versions
['1.2.1', '2.0.0']

feu.version.latest_minor_versions

latest_minor_versions(versions: Sequence[str]) -> list[str]

Return the latest version for each minor version in a list of semantic versions.

This function takes a list of semantic version strings (e.g. "1.0.0", "1.0.1", "1.1.0", "2.0.0"), groups them by their major and minor version numbers, and returns only the latest version from each minor group (based on the patch number).

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings in semantic version format.

required

Returns:

Type Description
list[str]

A list containing the latest version for each minor version, sorted by major and minor version numbers.

Example
>>> from feu.version import latest_major_versions
>>> versions = latest_minor_versions(["1.0.0", "1.0.1", "1.1.0", "1.1.2", "2.0.0", "2.0.3"])
>>> versions
['1.0.1', '1.1.2', '2.0.3']

feu.version.latest_version

latest_version(versions: Sequence[str]) -> str

Return the latest version string in a list of version identifiers.

This function compares version strings according to the PEP 440 specification using :class:packaging.version.Version. It supports standard releases, pre-releases (alpha, beta, release candidates), development releases, post releases, and epoch-based versions.

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings to compare.

required

Returns:

Type Description
str

The highest (latest) version in the list based on PEP 440 ordering.

Raises:

Type Description
ValueError

If versions is empty.

Example
>>> import operator
>>> from feu.version import latest_version
>>> latest_version(["1.0.0", "1.0.1rc1", "1.0.1"])
'1.0.1'
>>> latest_version(["1.2.0", "2.0.0a1"])
'2.0.0a1'

feu.version.normalize_package_name

normalize_package_name(name: str) -> str

Normalize a package name per PEP 508.

Converts the name to lowercase and replaces hyphens with underscores, so that "scikit-learn", "scikit_learn", and "Scikit_Learn" all compare as equal.

Parameters:

Name Type Description Default
name str

The package name to normalize.

required

Returns:

Type Description
str

The normalized package name.

Example
>>> from feu.version import normalize_package_name
>>> normalize_package_name("Scikit-Learn")
'scikit_learn'

feu.version.partition_package_bounds

partition_package_bounds(
    packages: Sequence[PackageBounds], names: Sequence[str]
) -> tuple[list[PackageBounds], list[PackageBounds]]

Split a sequence of PackageBounds into matched and unmatched by name.

Parameters:

Name Type Description Default
packages Sequence[PackageBounds]

A sequence of PackageBounds instances to filter.

required
names Sequence[str]

The package names to match against.

required

Returns:

Type Description
list[PackageBounds]

A tuple of two lists, both in the order they appear in packages:

list[PackageBounds]
  • The first list contains the PackageBounds instances whose name appears in names.
tuple[list[PackageBounds], list[PackageBounds]]
  • The second list contains the PackageBounds instances whose name does not appear in names.
Example
>>> from feu.version import PackageBounds, partition_package_bounds
>>> packages = [
...     PackageBounds(
...         name="numpy", lower="1.21", upper="2.0", section="project.dependencies"
...     ),
...     PackageBounds(
...         name="torch", lower="2.0", upper=None, section="project.dependencies"
...     ),
... ]
>>> matched, unmatched = partition_package_bounds(packages, ["numpy"])
>>> matched
[PackageBounds(name='numpy', lower='1.21', upper='2.0', section='project.dependencies')]
>>> unmatched
[PackageBounds(name='torch', lower='2.0', upper=None, section='project.dependencies')]

feu.version.read_pyproject_dependencies

read_pyproject_dependencies(
    path: str | Path,
) -> list[PackageBounds]

Read a pyproject.toml file and return the bounds for all packages defined in [project.dependencies].

Parameters:

Name Type Description Default
path str | Path

Path to the pyproject.toml file.

required

Returns:

Type Description
list[PackageBounds]

A list of PackageBounds instances, one per entry in

list[PackageBounds]

[project.dependencies]. Returns an empty list if the section

list[PackageBounds]

is absent or empty.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

TOMLDecodeError

If the file is not valid TOML.

Example
bounds = read_pyproject_dependencies("pyproject.toml")
for b in bounds:
    print(b.name, b.lower, b.upper)

feu.version.read_pyproject_optional_dependencies

read_pyproject_optional_dependencies(
    path: str | Path,
) -> list[PackageBounds]

Read a pyproject.toml file and return the bounds for all packages defined in [project.optional-dependencies].

All groups under [project.optional-dependencies] are included. The section field of each returned PackageBounds identifies the group, e.g. 'project.optional-dependencies.dev'.

Parameters:

Name Type Description Default
path str | Path

Path to the pyproject.toml file.

required

Returns:

Type Description
list[PackageBounds]

A list of PackageBounds instances, one per entry across all

list[PackageBounds]

optional-dependency groups, in the order they appear in the file.

list[PackageBounds]

Returns an empty list if the section is absent or empty.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

TOMLDecodeError

If the file is not valid TOML.

Example
bounds = read_pyproject_optional_dependencies("pyproject.toml")
for b in bounds:
    print(b.name, b.section, b.lower, b.upper)

feu.version.read_pyproject_package_bounds

read_pyproject_package_bounds(
    path: str | Path, package: str
) -> list[PackageBounds]

Read a pyproject.toml file and return the version bounds for a package.

Searches the following standard sections:

  • [project.dependencies]
  • [project.optional-dependencies.*]
  • [dependency-groups.*]

The package name comparison is case-insensitive and treats hyphens and underscores as equivalent, following PEP 508 normalisation rules.

Parameters:

Name Type Description Default
path str | Path

Path to the pyproject.toml file.

required
package str

The name of the package to look up.

required

Returns:

Type Description
list[PackageBounds]

A list of PackageBounds instances, one per occurrence of the

list[PackageBounds]

package across all sections. Returns an empty list if the package

list[PackageBounds]

is not found.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

TOMLDecodeError

If the file is not valid TOML.

Example
bounds = find_package_bounds("pyproject.toml", "numpy")
for b in bounds:
    print(b.section, b.lower, b.upper)

feu.version.sort_versions

sort_versions(
    versions: Sequence[str], reverse: bool = False
) -> list[str]

Sort a list of version strings in ascending or descending order.

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings.

required
reverse bool

If False, sort in ascending order; if True, sort in descending order.

False

Returns:

Type Description
list[str]

A new list of version strings sorted according to semantic version order.

Example
>>> import operator
>>> from feu.version import sort_versions
>>> sort_versions(["1.0.0", "1.2.0", "1.1.0"])
['1.0.0', '1.1.0', '1.2.0']
>>> sort_versions(["1.0.0", "1.2.0", "1.1.0"], reverse=True)
['1.2.0', '1.1.0', '1.0.0']

feu.version.unique_versions

unique_versions(versions: Sequence[str]) -> list[str]

Return a list of unique versions while preserving order.

Parameters:

Name Type Description Default
versions Sequence[str]

A list of version strings.

required

Returns:

Type Description
list[str]

A list containing only unique version strings, preserving the original order of first occurrence.

Example
>>> from feu.version import unique_versions, sort_versions
>>> versions = sort_versions(unique_versions(["1.0.0", "1.0.1", "1.0.0", "1.2.0"]))
>>> versions
['1.0.0', '1.0.1', '1.2.0']