diff --git a/doc/JobTemplate/AllInOne/CompletePipeline.rst b/doc/JobTemplate/AllInOne/CompletePipeline.rst index ea7168a..cb6bc90 100644 --- a/doc/JobTemplate/AllInOne/CompletePipeline.rst +++ b/doc/JobTemplate/AllInOne/CompletePipeline.rst @@ -159,6 +159,13 @@ It can be used for simple Python packages as well as namespace packages. * :ref:`pyTooling/Actions/.github/workflows/Parameters.yml ` * :ref:`pyTooling/Actions/.github/workflows/ExtractConfiguration.yml ` + + * :gh:`actions/checkout` + * :gh:`actions/setup-python` + + * :pypi:`wheel` + * :pypi:`tomli` + * :ref:`pyTooling/Actions/.github/workflows/UnitTesting.yml ` * :ref:`pyTooling/Actions/.github/workflows/ApplicationTesting.yml ` * :ref:`pyTooling/Actions/.github/workflows/CheckDocumentation.yml ` @@ -201,7 +208,7 @@ It only requires a `name` parameter to create the artifact names. Params: uses: pyTooling/Actions/.github/workflows/CompletePipeline.yml@r5 with: - name: pyTooling + package_name: myPackage .. _JOBTMPL/CompletePipeline/Parameters: @@ -286,7 +293,6 @@ package_namespace needs to be specified using this parameter. |br| In case of a simple Python package, this parameter must be specified as an empty string (``''``), which is the default. - :Example: .. grid:: 2 @@ -377,7 +383,9 @@ unittest_python_version :Type: string :Required: no :Default Value: ``'3.13'`` -:Possible Values: Any valid Python version conforming to the pattern ``major.minor``. +:Possible Values: Any valid Python version conforming to the pattern ``.`` or ``pypy-.``. |br| + See `actions/python-versions - available Python versions `__ + and `actions/setup-python - configurable Python versions `__. :Description: The default Python version used for intermediate jobs using Python tools. In case :ref:`JOBTMPL/CompletePipeline/Input/unittest_python_version_list` is empty, this default @@ -457,7 +465,9 @@ apptest_python_version :Type: string :Required: no :Default Value: ``'3.13'`` -:Possible Values: Any valid Python version conforming to the pattern ``major.minor``. +:Possible Values: Any valid Python version conforming to the pattern ``.`` or ``pypy-.``. |br| + See `actions/python-versions - available Python versions `__ + and `actions/setup-python - configurable Python versions `__. :Description: The default Python version used for intermediate jobs using Python tools. In case :ref:`JOBTMPL/CompletePipeline/Input/apptest_python_version_list` is empty, this default diff --git a/doc/JobTemplate/Setup/ExtractConfiguration.rst b/doc/JobTemplate/Setup/ExtractConfiguration.rst index 79fce1d..1e102ec 100644 --- a/doc/JobTemplate/Setup/ExtractConfiguration.rst +++ b/doc/JobTemplate/Setup/ExtractConfiguration.rst @@ -3,27 +3,47 @@ ExtractConfiguration #################### -The ``ExtractConfiguration`` job template is a workaround for the limitations of GitHub Actions to handle global variables in -GitHub Actions workflows (see `actions/runner#480 `__. +The ``ExtractConfiguration`` job template is a ..... -It generates output parameters with artifact names and a job matrix to be used in later running jobs. -**Behavior:** -.. todo:: Parameters:Behavior Needs documentation. +.. topic:: Features -**Dependencies:** + * Concatenate :ref:`JOBTMPL/ExtractConfiguration/Input/package_namespace` and :ref:`JOBTMPL/ExtractConfiguration/Input/package_name` + to :ref:`JOBTMPL/ExtractConfiguration/Output/package_fullname` and :ref:`JOBTMPL/ExtractConfiguration/Output/package_directory`. + * Provide commands to prepare the source code directory structure suitable for mypy. + * Extract the unittest XML report file (pytest JUnit XML) as directory name, filename and full path from :file:`pyproject.toml`. + * Extract the merged unittest XML report file as directory name, filename and full path from :file:`pyproject.toml`. + * Extract code coverage report HTML directory from :file:`pyproject.toml`. + * Extract code coverage XML report file as directory name, filename and full path from :file:`pyproject.toml`. + * Extract code coverage JSON report file as directory name, filename and full path from :file:`pyproject.toml`. -*None* +.. topic:: Behavior: + + .. todo:: ExtractConfiguration:Behavior needs documentation. + +.. topic:: Job Execution + + .. image:: ../../_static/pyTooling-Actions-ExtractConfiguration.png + :width: 600px + +.. topic:: Dependencies + + * :gh:`actions/checkout` + * :gh:`actions/setup-python` + + * :pypi:`wheel` + * :pypi:`tomli` + + +.. _JOBTMPL/ExtractConfiguration/Instantiation: Instantiation ************* -Simple Example -============== - -The following instantiation example creates a job `Params` derived from job template `Parameters` version `r0`. It only -requires a `name` parameter to create the artifact names. +The following instantiation example creates a job ``ConfigParams`` derived from job template ``ExtractConfiguration`` +version ``@r5``. It only requires a :ref:`JOBTMPL/ExtractConfiguration/Input/package_name` parameter to extract unit +test (pytest) and code coverage (Coverage.py) settings. .. code-block:: yaml @@ -34,20 +54,540 @@ requires a `name` parameter to create the artifact names. workflow_dispatch: jobs: - Params: + ConfigParams: uses: pyTooling/Actions/.github/workflows/ExtractConfiguration.yml@r5 with: - name: pyTooling + package_name: myPackage -Parameters -********** + UnitTesting: + uses: pyTooling/Actions/.github/workflows/UnitTesting.yml@r5 + needs: + - ConfigParams + with: + unittest_report_xml_directory: ${{ needs.ConfigParams.outputs.unittest_report_xml_directory }} + unittest_report_xml_filename: ${{ needs.ConfigParams.outputs.unittest_report_xml_filename }} + coverage_report_xml_directory: ${{ needs.ConfigParams.outputs.coverage_report_xml_directory }} + coverage_report_xml_filename: ${{ needs.ConfigParams.outputs.coverage_report_xml_filename }} + coverage_report_json_directory: ${{ needs.ConfigParams.outputs.coverage_report_json_directory }} + coverage_report_json_filename: ${{ needs.ConfigParams.outputs.coverage_report_json_filename }} + coverage_report_html_directory: ${{ needs.ConfigParams.outputs.coverage_report_html_directory }} +.. seealso:: + + :ref:`JOBTMPL/UnitTesting` + ``UnitTesting`` is usualy + :ref:`JOBTMPL/StaticTypeCheck` + xxx + :ref:`JOBTMPL/CheckDocumentation` + xxx + :ref:`JOBTMPL/InstallPackage` + xxx + :ref:`JOBTMPL/PublishCoverageResults` + xxx + :ref:`JOBTMPL/PublishTestResults` + xxx + :ref:`JOBTMPL/SphinxDocumentation` + xxx + +.. _JOBTMPL/ExtractConfiguration/Parameters: + +Parameter Summary +***************** + +.. rubric:: Goto :ref:`input parameters ` + ++---------------------------------------------------------------------+----------+----------+-------------------------------------------------------------------+ +| Parameter Name | Required | Type | Default | ++=====================================================================+==========+==========+===================================================================+ +| :ref:`JOBTMPL/ExtractConfiguration/Input/ubuntu_image_version` | no | string | ``'24.04'`` | ++---------------------------------------------------------------------+----------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Input/python_version` | no | string | ``'3.13'`` | ++---------------------------------------------------------------------+----------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Input/package_namespace` | no | string | ``''`` | ++---------------------------------------------------------------------+----------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Input/package_name` | yes | string | — — — — | ++---------------------------------------------------------------------+----------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Input/coverage_config` | no | string | ``'pyproject.toml'`` | ++---------------------------------------------------------------------+----------+----------+-------------------------------------------------------------------+ + +.. rubric:: Goto Goto :ref:`secrets ` + +This job template needs no secrets. + +.. rubric:: Goto Goto :ref:`output parameters ` + ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| Result Name | Type | Description | ++=================================================================================+==========+===================================================================+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/package_fullname` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/package_directory` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/mypy_prepare_command` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_report_xml_directory` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_report_xml_filename` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_report_xml` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_merged_report_xml_directory` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_merged_report_xml_filename` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_merged_report_xml` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_html_directory` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_xml_directory` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_xml_filename` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_xml` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_json_directory` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_json_filename` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ +| :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_json` | string | | ++---------------------------------------------------------------------------------+----------+-------------------------------------------------------------------+ + + +.. _JOBTMPL/ExtractConfiguration/Inputs: + +Input Parameters +**************** + +.. _JOBTMPL/ExtractConfiguration/Input/ubuntu_image_version: + +ubuntu_image_version +==================== + +:Type: string +:Required: no +:Default Value: ``'24.04'`` +:Possible Values: See `actions/runner-images - Available Images `__ + for available Ubuntu image versions. +:Description: Version of the Ubuntu image used to run this job. + + .. note:: + + Unfortunately, GitHub Actions has only a `limited set of functions `__, + thus, the usual Ubuntu image name like ``'ubuntu-24.04'`` can't be split into image name and image + version. + + +.. _JOBTMPL/ExtractConfiguration/Input/python_version: + +python_version +============== + +:Type: string +:Required: no +:Default Value: ``'3.13'`` +:Possible Values: Any valid Python version conforming to the pattern ``.`` or ``pypy-.``. |br| + See `actions/python-versions - available Python versions `__ + and `actions/setup-python - configurable Python versions `__. +:Description: Python version used to run Python code in this job. + + +.. _JOBTMPL/ExtractConfiguration/Input/package_namespace: + +package_namespace +================= + +:Type: string +:Required: no +:Default Value: ``''`` +:Possible Values: Any valid Python namespace. +:Description: In case the package is a Python namespace package, the name of the library's or package's namespace + needs to be specified using this parameter. |br| + In case of a simple Python package, this parameter must be specified as an empty string (``''``), + which is the default. +:Example: + .. grid:: 2 + + .. grid-item:: + :columns: 5 + + .. rubric:: Example Instantiation + + .. code-block:: yaml + + name: Pipeline + + jobs: + ConfigParams: + uses: pyTooling/Actions/.github/workflows/ExtractConfiguration.yml@r5 + with: + package_namespace: myFramework + package_name: Extension + + .. grid-item:: + :columns: 4 + + .. rubric:: Example Directory Structure + + .. code-block:: + + 📂ProjectRoot/ + 📂myFramework/ + 📂Extension/ + 📦SubPackage/ + 🐍__init__.py + 🐍SubModuleA.py + 🐍__init__.py + 🐍ModuleB.py + + +.. _JOBTMPL/ExtractConfiguration/Input/package_name: + +package_name +============ + +:Type: string +:Required: no +:Default Value: ``''`` +:Possible Values: Any valid Python package name. +:Description: In case of a simple Python package, this package's name is specified using this parameter. |br| + In case the package is a Python namespace package, the parameter + :ref:`JOBTMPL/ExtractConfiguration/Input/package_namespace` must be specified, too. +:Example: + .. grid:: 2 + + .. grid-item:: + :columns: 5 + + .. rubric:: Example Instantiation + + .. code-block:: yaml + + name: Pipeline + + jobs: + ConfigParams: + uses: pyTooling/Actions/.github/workflows/ExtractConfiguration.yml@r5 + with: + package_name: myPackage + + .. grid-item:: + :columns: 4 + + .. rubric:: Example Directory Structure + + .. code-block:: + + 📂ProjectRoot/ + 📂myFramework/ + 📦SubPackage/ + 🐍__init__.py + 🐍SubModuleA.py + 🐍__init__.py + 🐍ModuleB.py + + +.. _JOBTMPL/ExtractConfiguration/Input/coverage_config: + +coverage_config +=============== + +:Type: string +:Required: no +:Default Value: ``'pyproject.toml'`` +:Possible Values: Any valid path to a :file:`pyproject.toml` file. |br| + Alternatively, path to a :file:`.coveragerc` file (deprecated). +:Description: Path to a Python project configuration file for extraction of project settings. +:Example: + .. grid:: 2 + + .. grid-item:: + :columns: 5 + + .. rubric:: Extracted values from :file:`pyproject.toml` + + .. code-block:: toml + + [tool.pytest] + junit_xml = "report/unit/UnittestReportSummary.xml" + + [tool.pyedaa-reports] + junit_xml = "report/unit/unittest.xml" + + [tool.coverage.xml] + output = "report/coverage/coverage.xml" + + [tool.coverage.json] + output = "report/coverage/coverage.json" + + [tool.coverage.html] + directory = "report/coverage/html" + title="Code Coverage of pyDummy" + + +.. _JOBTMPL/ExtractConfiguration/Secrets: + Secrets ******* +This job template needs no secrets. -Results + +.. _JOBTMPL/ExtractConfiguration/Outputs: + +Outputs ******* +.. _JOBTMPL/ExtractConfiguration/Output/package_fullname: +package_fullname +================ + +:Type: string +:Description: Returns the full package name composed from :ref:`JOBTMPL/ExtractConfiguration/Input/package_namespace` + and :ref:`JOBTMPL/ExtractConfiguration/Input/package_name`. +:Example: ``myFramework.Extension`` + + +.. _JOBTMPL/ExtractConfiguration/Output/package_directory: + +package_directory +================= + +:Type: string +:Description: Returns the full package path composed from :ref:`JOBTMPL/ExtractConfiguration/Input/package_namespace` + and :ref:`JOBTMPL/ExtractConfiguration/Input/package_name`. +:Example: ``myFramework/Extension`` + + +.. _JOBTMPL/ExtractConfiguration/Output/mypy_prepare_command: + +mypy_prepare_command +==================== + +:Type: string +:Description: Returns a preparation command for `mypy `__. |br| + In case the Python package is a namespace package, an :file:`__init__.py` must be created, otherwise + mypy has problems analyzing the namespace package. +:Example: ``touch myFramework/__init__.py`` + +.. _JOBTMPL/ExtractConfiguration/Output/unittest_report_xml_directory: + +unittest_report_xml_directory +============================= + +:Type: string +:Description: Returns the directory where the unittest XML report file (`pytest `__ JUnit XML) + will be created. |br| + This is the directory portion of :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_report_xml`. +:Example: :file:`reports/unit` +:pyproject.toml: + .. code-block:: toml + + [tool.pytest] + junit_xml = "report/unit/UnittestReportSummary.xml" + + +.. _JOBTMPL/ExtractConfiguration/Output/unittest_report_xml_filename: + +unittest_report_xml_filename +============================ + +:Type: string +:Description: Returns the filename of the unittest XML report file (`pytest `__ JUnit XML). |br| + This is the filename portion of :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_report_xml`. +:Example: :file:`UnittestReportSummary.xml` +:pyproject.toml: + .. code-block:: toml + + [tool.pytest] + junit_xml = "report/unit/UnittestReportSummary.xml" + + +.. _JOBTMPL/ExtractConfiguration/Output/unittest_report_xml: + +unittest_report_xml +=================== + +:Type: string +:Description: Returns the path where the unittest XML report file (`pytest `__ JUnit XML) + will be created. |br| + This is the concatenation of :ref:`directory portion ` + and :ref:`filename portion `. +:Example: :file:`reports/unit/UnittestReportSummary.xml` +:pyproject.toml: + .. code-block:: toml + + [tool.pytest] + junit_xml = "report/unit/UnittestReportSummary.xml" + + +.. _JOBTMPL/ExtractConfiguration/Output/unittest_merged_report_xml_directory: + +unittest_merged_report_xml_directory +==================================== + +:Type: string +:Description: Returns the directory where the merged unittest XML report file + (`pyedaa-reports `__ JUnit XML) will be created. |br| + This is the directory portion of :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_merged_report_xml`. +:Example: :file:`reports/unit` +:pyproject.toml: + .. code-block:: toml + + [tool.pyedaa-reports] + junit_xml = "report/unit/unittest.xml" + + +.. _JOBTMPL/ExtractConfiguration/Output/unittest_merged_report_xml_filename: + +unittest_merged_report_xml_filename +=================================== + +:Type: string +:Description: Returns the filename of the merged unittest XML report + (`pyedaa-reports `__ JUnit XML). |br| + This is the filename portion of :ref:`JOBTMPL/ExtractConfiguration/Output/unittest_merged_report_xml`. +:Example: :file:`unittest.xml` +:pyproject.toml: + .. code-block:: toml + + [tool.pyedaa-reports] + junit_xml = "report/unit/unittest.xml" + + +.. _JOBTMPL/ExtractConfiguration/Output/unittest_merged_report_xml: + +unittest_merged_report_xml +========================== + +:Type: string +:Description: Returns the path where the merged unittest XML report file (`pyedaa-reports `__ JUnit XML) + will be created. |br| + This is the concatenation of :ref:`directory portion ` + and :ref:`filename portion `. +:Example: :file:`report/unit/unittest.xml` +:pyproject.toml: + .. code-block:: toml + + [tool.pyedaa-reports] + junit_xml = "report/unit/unittest.xml" + + +.. _JOBTMPL/ExtractConfiguration/Output/coverage_report_html_directory: + +coverage_report_html_directory +============================== + +:Type: string +:Description: Returns the directory where the code coverage HTML report (`Coverage.py `__) + will be generated. +:Example: :file:`report/coverage/html` +:pyproject.toml: + .. code-block:: toml + + [tool.coverage.html] + directory = "report/coverage/html" + title="Code Coverage of pyDummy" + + +.. _JOBTMPL/ExtractConfiguration/Output/coverage_report_xml_directory: + +coverage_report_xml_directory +============================= + +:Type: string +:Description: Returns the directory where the code coverage XML report file (`Coverage.py `__) + will be created. |br| + This is the directory portion of :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_xml`. +:Example: :file:`reports/coverage` +:pyproject.toml: + .. code-block:: toml + + [tool.coverage.xml] + output = "report/coverage/coverage.xml" + + +.. _JOBTMPL/ExtractConfiguration/Output/coverage_report_xml_filename: + +coverage_report_xml_filename +============================ + +:Type: string +:Description: Returns the filename of the code coverage XML report file (`Coverage.py `__). |br| + This is the filename portion of :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_xml`. +:Example: :file:`coverage.xml` +:pyproject.toml: + .. code-block:: toml + + [tool.coverage.xml] + output = "report/coverage/coverage.xml" + + +.. _JOBTMPL/ExtractConfiguration/Output/coverage_report_xml: + +coverage_report_xml +=================== + +:Type: string +:Description: Returns the path where the code coverage XML report file (`Coverage.py `__) + will be created. |br| + This is the concatenation of :ref:`directory portion ` + and :ref:`filename portion `. +:Example: :file:`report/coverage/coverage.xml` +:pyproject.toml: + .. code-block:: toml + + [tool.coverage.xml] + output = "report/coverage/coverage.xml" + + +.. _JOBTMPL/ExtractConfiguration/Output/coverage_report_json_directory: + +coverage_report_json_directory +============================== + +:Type: string +:Description: Returns the directory where the code coverage JSON report file (`Coverage.py `__) + will be created. |br| + This is the directory portion of :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_json`. +:Example: :file:`reports/coverage` +:pyproject.toml: + .. code-block:: toml + + [tool.coverage.json] + output = "report/coverage/coverage.json" + + +.. _JOBTMPL/ExtractConfiguration/Output/coverage_report_json_filename: + +coverage_report_json_filename +============================= + +:Type: string +:Description: Returns the filename of the code coverage JSON report file (`Coverage.py `__). |br| + This is the filename portion of :ref:`JOBTMPL/ExtractConfiguration/Output/coverage_report_json`. +:Example: :file:`coverage.json` +:pyproject.toml: + .. code-block:: toml + + [tool.coverage.json] + output = "report/coverage/coverage.json" + + +.. _JOBTMPL/ExtractConfiguration/Output/coverage_report_json: + +coverage_report_json +==================== + +:Type: string +:Description: Returns the path where the code coverage JSON report file (`Coverage.py `__) + will be created. |br| + This is the concatenation of :ref:`directory portion ` + and :ref:`filename portion `. +:Example: :file:`report/coverage/coverage.json` +:pyproject.toml: + .. code-block:: toml + + [tool.coverage.json] + output = "report/coverage/coverage.json" diff --git a/doc/JobTemplate/index.rst b/doc/JobTemplate/index.rst index 870263b..7c2dbbf 100644 --- a/doc/JobTemplate/index.rst +++ b/doc/JobTemplate/index.rst @@ -87,5 +87,7 @@ python_version :Type: string :Required: usually no :Default Value: ``'3.13'`` -:Possible Values: See `actions/runner-images - Available Images `__ +:Possible Values: Any valid Python version conforming to the pattern ``.`` or ``pypy-.``. |br| + See `actions/python-versions - available Python versions `__ + and `actions/setup-python - configurable Python versions `__. :Description: Python version used to run Python scripts in a job. diff --git a/doc/_static/pyTooling-Actions-ExtractConfiguration.png b/doc/_static/pyTooling-Actions-ExtractConfiguration.png new file mode 100644 index 0000000..a29c2a7 Binary files /dev/null and b/doc/_static/pyTooling-Actions-ExtractConfiguration.png differ diff --git a/doc/conf.py b/doc/conf.py index 2b2071f..1b683e0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -214,11 +214,12 @@ autodoc_typehints = "both" # Sphinx.Ext.ExtLinks # ============================================================================== extlinks = { - "gh": (f"https://GitHub.com/%s", "gh:%s"), + "gh": (f"https://GitHub.com/%s", "GitHub: %s"), "ghissue": (f"https://GitHub.com/{githubNamespace}/{githubProject}/issues/%s", "issue #%s"), "ghpull": (f"https://GitHub.com/{githubNamespace}/{githubProject}/pull/%s", "pull request #%s"), "ghsrc": (f"https://GitHub.com/{githubNamespace}/{githubProject}/blob/main/%s", None), "wiki": (f"https://en.wikipedia.org/wiki/%s", None), + "pypi": (f"https://pypi.org/project/%s/", "PyPI: %s"), }