diff --git a/doc/source/how-to/packaging.rst b/doc/source/how-to/packaging.rst index 80e2d02ea..6b9e9c2d0 100644 --- a/doc/source/how-to/packaging.rst +++ b/doc/source/how-to/packaging.rst @@ -400,6 +400,100 @@ Examples of PyAnsys projects that have these optional dependencies are: - `PyAnsys Geometry targets `_ - `PyACP targets `_ +Dependency version range +------------------------ + +.. note:: + + This guidance applies only to PyAnsys *library* projects. For projects + which deliver an *application* or *dedicated workflow*, it is + recommended to fully pinning all (direct and transitive) dependencies. + +When specifying dependencies in a project, it is generally recommended to avoid +setting upper version limits unless it is absolutely necessary. The reason for +that is because arbitrarily restricting a dependency to be lower than a +certain version (for example `numpy<2.0`) can prevent your project from working +with newer and perfectly compatible versions, and often causes more problems +than it solves. Such restrictions limit forward compatibility, block users from +upgrading dependencies, and increase the risk of version conflicts. + +This issue is even more critical in the context of the PyAnsys `metapackage`_ +which install many PyAnsys projects. In this setup, having strict upper bounds +on versions can easily result in unsatisfiable dependency constraints across +the ecosystem. For instance, if a package declares a dependency on `numpy<2.0` +despite being compatible with later versions, and another package requires +`numpy>=2.0.0` to leverage a new feature, it becomes impossible to install +both packages simultaneously. This occurs even though no actual incompatibility +exists between them, and it can lead to frustration for users and maintainers +as it prevents otherwise compatible packages from being used together seamlessly. + +It is better to define only a minimum version (`>=`) and rely on Continuous +Integration (CI) to detect real breakages as dependencies evolve. If a future +version does introduce a breaking change, you can then add an upper bound with +a clear explanation. For example: + +.. code-block:: toml + + [project] + dependencies = [ + "numpy<2.0", # breaking changes in Python and C APIs'. + ] + +Setting a lower bound (`>=`) is considered good practice for multiple reasons. +First, it documents the oldest version of a dependency that your project +explicitly supports. It is often the oldest version that is compatible with +the Python versions you support. For example, if your project supports +Python versions from `3.11` to `3.13`, you need to ensure that all dependencies +are compatible with at least Python `3.11`. This is important for users who may +be using older versions of Python and want to ensure compatibility with your +project. In other cases, the lower bound is related to the version where +certain key features your code relies on were first introduced. For instance, +if your code uses an API or behavior that only appeared in version `1.3`, +setting `>=1.3` communicates both a technical requirement and an implicit +contract to your users and contributors. + +This helps avoiding unexpected breakages when someone installs your project +in an environment with older versions of dependencies. Rather than encountering +obscure runtime errors or missing features, the version constraint prevents +your project to be installed. It also helps to maintain clarity for long-term +maintenance and simplifies debugging. + +Below is an example of a dependency specification that follows these guidelines: + +.. tab-set:: + + .. tab-item:: flit + + .. code-block:: toml + + [project] + dependencies = [ + "matplotlib>=3.5.2", + "numpy>=1.20.0", + ] + + .. tab-item:: poetry + + .. code-block:: toml + + [tool.poetry.dependencies] + matplotlib = ">=3.5.2" + numpy = ">=1.20.0" + + .. tab-item:: setuptools + + .. code-block:: python + + from setuptools import setup + + setup( + ..., + install_requires=[ + "matplotlib >= 3.5.2", + "numpy >= 1.20.0", + ], + ) + Dependabot ----------