.. _PACKAGING: Overview ######## The module :mod:`pyTooling.Packaging` provides helper functions to achieve a *single-source-of-truth* Python package description, where (almost) no information is duplicated. The main idea is to read configuration files, READMEs, and Python source files from ``setup.py``, so it doesn't duplicate information. This allows an easier the maintenance of Python packages. .. #contents:: Table of Contents :depth: 2 .. _PACKAGING/Helper: Helper Functions ################ The following helper functions are used by :func:`~pyTooling.Packaging.DescribePythonPackage`, but these can also be called individually to reuse internal features offered by that package description function. .. _PACKAGING/Helper/loadReadmeFile: loadReadmeFile ************** The function :func:`~pyTooling.Packaging.loadReadmeFile` reads a ``README`` file and guesses the contents MIME type on the file's extension. It returns an instance of :class:`~pyTooling.Packaging.Readme`. This read text can then be used for the package's *long description*. .. topic:: Supported file formats * ``*.txt`` - Plain text * ``*.md`` - `Markdown `__ (further reading: :wiki:`Markdown`) * ``*.rst`` - `ReStructured Text `__ (further reading: :wiki:`ReStructuredText`) .. grid:: 2 .. grid-item:: :columns: 6 .. admonition:: Usage in a ``setup.py`` .. code-block:: Python from pathlib import Path from pyTooling.Packaging import loadReadmeFile readmeFile = Path("README.md") readme = loadReadmeFile(readmeFile) # print(readme.Content) # print(readme.MimeType) .. grid-item:: :columns: 6 .. admonition:: ``README.md`` .. code-block:: Markdown # pyTooling **pyTooling** is a powerful collection of arbitrary useful abstract data models, missing classes, decorators, a new performance boosting meta-class and enhanced exceptions. It also provides lots of helper functions e.g. to ease the handling of package descriptions or to unify multiple existing APIs into a single API. .. _PACKAGING/Helper/loadRequirementsFile: loadRequirementsFile ******************** The function :func:`~pyTooling.Packaging.loadRequirementsFile` recursively reads a ``requirements.txt`` file and extracts all specified dependencies. As a result, a list of requirement strings is returned. .. topic:: Features * Comments are skipped. * Recursive references are followed. * Special dependency entries like Git repository references are translates to match the syntax expected by setuptools. .. warning:: The returned list might contain duplicates, which should be removed before further processing. This can be achieve by converting the result to a :class:`set` and back to a :class:`list`. .. code-block:: Python requirements = list(set(loadRequirementsFile(requirementsFile))) .. grid:: 2 .. grid-item:: :columns: 6 .. admonition:: Usage in a ``setup.py`` .. code-block:: Python from pathlib import Path from pyTooling.Packaging import loadRequirementsFile requirementsFile = Path("doc/requirements.txt") requirements = loadRequirementsFile(requirementsFile) # for req in requirements: # print(req) .. grid-item:: :columns: 6 .. admonition:: ``requirements.txt`` .. code-block:: -r ../requirements.txt Sphinx ~= 8.2 docutils <= 0.21 sphinx_rtd_theme ~= 3.0 .. _PACKAGING/Helper/extractVersionInformation: extractVersionInformation ************************* The function :func:`~pyTooling.Packaging.extractVersionInformation` extracts version information from a Python source file (module). Usually these module variables are defined in a ``__init__.py`` file. .. rubric:: Supported fields * Author name (``__author__``) * Author email address (``__email__``) * Copyright information (``__copyright_``) * License name (``__license__``) * Version number (``__version__``) * Keywords (``__keywords__``) The function returns an instance of :class:`~pyTooling.Packaging.VersionInformation`, which offers the gathered information as properties. .. grid:: 2 .. grid-item:: :columns: 6 .. admonition:: Usage in ``setup.py`` .. code-block:: python from setuptools import setup from pyTooling.Packaging import extractVersionInformation file = Path("./pyTooling/Common/__init__.py") versionInfo = extractVersionInformation(file) setup( # ... version=versionInformation.Version, author=versionInformation.Author, author_email=versionInformation.Email, keywords=versionInformation.Keywords, # ... ) .. grid-item:: :columns: 6 .. admonition:: ``__init__.py`` .. code-block:: python __author__ = "Patrick Lehmann" __email__ = "Paebbels@gmail.com" __copyright__ = "2017-2024, Patrick Lehmann" __license__ = "Apache License, Version 2.0" __version__ = "1.10.1" __keywords__ = ["decorators", "meta classes", "exceptions", "platform", "versioning"] .. _PACKAGING/Descriptions: PackageDescriptions ################### .. _PACKAGING/Descriptions/Python: DescribePythonPackage ********************* :func:`~pyTooling.Packaging.DescribePythonPackage` is a helper function to describe a Python package. The result is a dictionary that can be handed over to :func:`setuptools.setup`. Some information will be gathered implicitly from well-known files (e.g. ``README.md``, ``requirements.txt``, ``__init__.py``). Handling of namespace packages ============================== If parameter ``packageName`` contains a dot, a namespace package is assumed. Then :func:`setuptools.find_namespace_packages` is used to discover package files. |br| Otherwise, the package is considered a normal package and :func:`setuptools.find_packages` is used. In both cases, the following packages (directories) are excluded from search: * ``build``, ``build.*`` * ``dist``, ``dist.*`` * ``doc``, ``doc.*`` * ``tests``, ``tests.*`` Handling of minimal Python version ================================== The minimal required Python version is selected from parameter ``pythonVersions``. Handling of dunder variables ============================ A Python source file specified by parameter ``sourceFileWithVersion`` will be analyzed with Pythons parser and the resulting AST will be searched for the following dunder variables: * ``__author__``: :class:`str` * ``__copyright__``: :class:`str` * ``__email__``: :class:`str` * ``__keywords__``: :class:`typing.Iterable`[:class:`str`] * ``__license__``: :class:`str` * ``__version__``: :class:`str` The gathered information be used to add further mappings in the result dictionary. Handling of package classifiers =============================== To reduce redundantly provided parameters to this function (e.g. supported ``pythonVersions``), only additional classifiers should be provided via parameter ``classifiers``. The supported Python versions will be implicitly converted to package classifiers, so no need to specify them in parameter ``classifiers``. The following classifiers are implicitly handled: license The license specified by parameter ``license`` is translated into a classifier. |br| See also :meth:`pyTooling.Licensing.License.PythonClassifier` Python versions Always add ``Programming Language :: Python :: 3 :: Only``. |br| For each value in ``pythonVersions``, one ``Programming Language :: Python :: Major.Minor`` is added. Development status The development status specified by parameter ``developmentStatus`` is translated to a classifier and added. .. seealso:: `Python package classifiers `__ Handling of extra requirements ============================== If additional requirement files are provided, e.g. requirements to build the documentation, then *extra* requirements are defined. These can be installed via ``pip install packageName[extraName]``. If so, an extra called ``all`` is added, so developers can install all dependencies needed for package development. ``doc`` If parameter ``documentationRequirementsFile`` is present, an extra requirements called ``doc`` will be defined. ``test`` If parameter ``unittestRequirementsFile`` is present, an extra requirements called ``test`` will be defined. ``build`` If parameter ``packagingRequirementsFile`` is present, an extra requirements called ``build`` will be defined. User-defined If parameter ``additionalRequirements`` is present, an extra requirements for every mapping entry in the dictionary will be added. ``all`` If any of the above was added, an additional extra requirement called ``all`` will be added, summarizing all extra requirements. Handling of keywords ==================== If parameter ``keywords`` is not specified, the dunder variable ``__keywords__`` from ``sourceFileWithVersion`` will be used. Otherwise, the content of the parameter, if not None or empty. .. _PACKAGING/Descriptions/GitHub: DescribePythonPackageHostedOnGitHub *********************************** :func:`~pyTooling.Packaging.DescribePythonPackageHostedOnGitHub` is a helper function to describe a Python package when the source code is hosted on GitHub. This is a wrapper for :func:`~pyTooling.Packaging.DescribePythonPackage`, because some parameters can be simplified by knowing the GitHub namespace and repository name: issue tracker URL, source code URL, ... .. todo:: normal packages ``PackageName`` namespace package root package ``NamespacePackage.*`` namespace package sub package ``NamespacePackage.PackageName`` deriving URLs .. admonition:: Usage in ``setup.py`` .. code-block:: Python from setuptools import setup from pathlib import Path from pyTooling.Packaging import DescribePythonPackageHostedOnGitHub packageName = "pyTooling.Packaging" setup( **DescribePythonPackageHostedOnGitHub( packageName=packageName, description="A set of helper functions to describe a Python package for setuptools.", gitHubNamespace="pyTooling", keywords="Python3 setuptools package wheel installation", sourceFileWithVersion=Path(f"{packageName.replace('.', '/')}/__init__.py"), developmentStatus="beta", pythonVersions=("3.8", "3.9", "3.10") ) )