From a20ffc1223b7dee8f3c732e237d7890f90f22017 Mon Sep 17 00:00:00 2001 From: JinWang An Date: Mon, 27 Mar 2023 17:02:32 +0900 Subject: [PATCH] Imported Upstream version 54.1.1 --- .bumpversion.cfg | 2 +- .github/ISSUE_TEMPLATE/bug-report.yml | 130 ++++++++++ .github/ISSUE_TEMPLATE/config.yml | 15 ++ .../ISSUE_TEMPLATE/documentation-report.yml | 93 +++++++ .github/ISSUE_TEMPLATE/feature-request.yml | 105 ++++++++ CHANGES.rst | 15 ++ docs/conf.py | 3 + docs/requirements.txt | 1 + docs/userguide/dependency_management.rst | 228 ++++++++++-------- docs/userguide/package_discovery.rst | 112 +++++---- docs/userguide/quickstart.rst | 43 +++- setup.cfg | 3 +- setuptools/dist.py | 13 + setuptools/tests/test_config.py | 19 ++ 14 files changed, 620 insertions(+), 162 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation-report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9a23064..1d2c227 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 54.1.0 +current_version = 54.1.1 commit = True tag = True diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..5f551a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,130 @@ +--- +name: 🐛 Bug report +description: >- + Create a report to help us improve when + something is not working correctly +title: '[BUG] ' +labels: +- bug +- Needs Triage +issue_body: false # default: true, adds a classic WSYWIG textarea, if on + +body: +- type: markdown + attributes: + value: > + **Thank you for wanting to report a bug in setuptools!** + + + ⚠ + Verify first that your issue is not + [already reported on GitHub][issue search] and keep in mind and + keep in mind that we may have to keep the current behavior because + [every change breaks someone's workflow][XKCD 1172]. + We try to be mindful about this. + + Also test if the latest release and main branch are affected too. + + + If you are seeking community support, please consider + [starting a discussion][Discussions]. + + + Thank you for your collaboration! + + + [Discussions]: https://github.com/pypa/setuptools/discussions + + [issue search]: https://github.com/pypa/setuptools/search?q=is%3Aissue&type=issues + + [XKCD 1172]: https://xkcd.com/1172/ + +- type: markdown + attributes: + value: >- + **Environment** +- type: input + attributes: + label: setuptools version + placeholder: For example, setuptools===60.4.2 + validations: + required: true +- type: input + attributes: + label: Python version + placeholder: For example, Python 3.10 + validations: + required: true +- type: input + attributes: + label: OS + placeholder: For example, Gentoo Linux, RHEL 8, Arch Linux, macOS etc. + validations: + required: true +- type: textarea + attributes: + label: Additional environment information + description: >- + Feel free to add more information about your environment here. + placeholder: >- + This is only happening when I run setuptools on my fridge's patched firmware 🤯 + +- type: textarea + attributes: + label: Description + description: >- + A clear and concise description of what the bug is. + placeholder: >- + I tried doing X and I expected it to result in Y because the docs + mentioned Z but what happened next what totally unexpected! + And here's why... + validations: + required: true + +- type: textarea + attributes: + label: Expected behavior + description: >- + A clear and concise description of what you expected to happen. + placeholder: >- + I tried doing X and I expected it to result in Y. I'm confused... + validations: + required: true + +- type: textarea + attributes: + label: How to Reproduce + description: >- + Describe the steps to reproduce this bug. + placeholder: | + 1. Integrate setuptools via '...' + 2. Then run '...' + 3. An error occurs. + validations: + required: true + +- type: textarea + attributes: + label: Output + description: >- + Paste the output of the steps above, including the commands + themselves and setuptools' output/traceback etc. + value: | + ```console + + ``` + validations: + required: true + + +- type: checkboxes + attributes: + label: Code of Conduct + description: | + Read the [PSF Code of Conduct][CoC] first. + + [CoC]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + options: + - label: I agree to follow the PSF Code of Conduct + required: true +... diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..dde102c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,15 @@ +# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser +blank_issues_enabled: false # default: true +contact_links: +- name: 🤔 Have questions or need support? + url: https://github.com/pypa/setuptools/discussions + about: This is a place for the community to exchange ideas and recipes +- name: 💬 Discourse + url: https://discuss.python.org/c/packaging + about: | + Please ask typical Q&A here: general ideas for Python packaging, + questions about structuring projects and so on +- name: >- + 💬 IRC: #pypa @ Freenode + url: https://webchat.freenode.net/#pypa + about: Chat with devs diff --git a/.github/ISSUE_TEMPLATE/documentation-report.yml b/.github/ISSUE_TEMPLATE/documentation-report.yml new file mode 100644 index 0000000..a947dad --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation-report.yml @@ -0,0 +1,93 @@ +--- +name: 📝 Documentation Report +title: '[Docs] ' +description: Ask us about docs +labels: +- documentation +- Needs Triage +# NOTE: issue body is enabled to allow screenshots +issue_body: true # default: true, adds a classic WSYWIG textarea, if on + +body: +- type: markdown + attributes: + value: > + **Thank you for wanting to report a problem with setuptools + documentation!** + + + Please fill out your suggestions below. If the problem seems + straightforward, feel free to go ahead and + submit a pull request instead! + + + ⚠ + Verify first that your issue is not [already reported on + GitHub][issue search]. + + + If you are seeking community support, please consider + [starting a discussion][Discussions]. + + + Thank you for your collaboration! + + + [issue search]: https://github.com/pypa/setuptools/search?q=is%3Aissue&type=issues + + [Discussions]: https://github.com/pypa/setuptools/discussions + +- type: textarea + attributes: + label: Summary + description: > + Explain the problem briefly below, add suggestions to wording + or structure. + + + **HINT:** Did you know the documentation has a `View on GitHub` + link on every page? Feel free to use it to start a pull request + right from the GitHub UI! + placeholder: >- + I was reading the setuptools documentation of version X and I'm + having problems understanding Y. It would be very helpful if that + got rephrased as Z. + validations: + required: true + +- type: textarea + attributes: + label: OS / Environment + description: >- + Provide all relevant information below, e.g. OS version, + browser, etc. + placeholder: Fedora 33, Firefox etc. + + +- type: checkboxes + attributes: + label: Code of Conduct + description: | + Read the [PSF Code of Conduct][CoC] first. + + [CoC]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + options: + - label: I agree to follow the PSF Code of Conduct + required: true + + +- type: markdown + attributes: + value: > + + + ### Additional Information + + + Describe how this improves the documentation, e.g. before/after + situation or screenshots. + + + **HINT:** You can paste https://gist.github.com links for + larger files. +... diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..7dec35a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,105 @@ +--- +name: ✨ Feature request +description: Suggest an idea for setuptools +title: '[FR] ' +labels: +- enhancement +- Needs Triage +issue_body: false # default: true, adds a classic WSYWIG textarea, if on + +body: +- type: markdown + attributes: + value: > + **Thank you for wanting to suggest a feature for setuptools!** + + + 💡 + Before you go ahead with your request, please first consider if it + would be useful for majority of the setuptools users. As a general + rule of thumb, any feature that is only of interest to a small sub + group should be implemented in a third-party plugin or maybe even + just your project alone. Be mindful of the fact that the core + setuptools features have a broad impact. + + +
+ + ❗ Every change breaks someone's workflow. + + + + [![❗ Every change breaks someone's workflow.](https://imgs.xkcd.com/comics/workflow.png) + ](https://xkcd.com/1172/) +
+ + + ⚠ + Verify first that your idea is not [already requested on GitHub][issue search]. + + + + [issue search]: https://github.com/pypa/setuptools/search?q=is%3Aissue&type=issues + +- type: textarea + attributes: + label: What's the problem this feature will solve? + description: >- + What are you trying to do, that you are unable to achieve + with setuptools as it currently stands? + placeholder: >- + I'm trying to do X and I'm missing feature Y for this to be + easily achievable. + validations: + required: true + +- type: textarea + attributes: + label: Describe the solution you'd like + description: > + Clear and concise description of what you want to happen. + + + Provide examples of real world use cases that this would enable + and how it solves the problem described above. + placeholder: >- + When I do X, I want to achieve Y in a situation when Z. + validations: + required: true + +- type: textarea + attributes: + label: Alternative Solutions + description: >- + Have you tried to workaround the problem using other tools? Or a + different approach to solving this issue? Please elaborate here. + placeholder: >- + I tried doing X, Y and Z. But they are subobpimal because of P. + +- type: textarea + attributes: + label: Additional context + description: > + Add any other context, links, etc. about the feature here. + Describe how the feature would be used, why it is needed and what + it would solve. + + + **HINT:** You can paste https://gist.github.com links for + larger files. + placeholder: >- + I asked on https://stackoverflow.com/.... and the community + advised me to do X, Y and Z. + + +- type: checkboxes + attributes: + label: Code of Conduct + description: | + Read the [PSF Code of Conduct][CoC] first. + + [CoC]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + options: + - label: I agree to follow the PSF Code of Conduct + required: true +... diff --git a/CHANGES.rst b/CHANGES.rst index 3920b27..bedd1fa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,18 @@ +v54.1.1 +------- + + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ +* #2584: Added ``sphinx-inline-tabs`` extension to allow for comparison of ``setup.py`` and its equivalent ``setup.cfg`` -- by :user:`amy-lei` + +Misc +^^^^ +* #2592: Made option keys in the ``[metadata]`` section of ``setup.cfg`` case-sensitive. Users having + uppercase option spellings will get a warning suggesting to make them to lowercase + -- by :user:`melissa-kun-li` + + v54.1.0 ------- diff --git a/docs/conf.py b/docs/conf.py index 0a5136b..d8996d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -96,3 +96,6 @@ default_role = 'any' html_theme = 'alabaster' templates_path = ['_templates'] html_sidebars = {'index': ['tidelift-sidebar.html']} + +# Add support for inline tabs +extensions += ['sphinx_inline_tabs'] diff --git a/docs/requirements.txt b/docs/requirements.txt index 104d68f..0292759 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,5 +3,6 @@ sphinx jaraco.packaging>=6.1 rst.linker>=1.9 pygments-github-lexers==0.0.5 +sphinx-inline-tabs setuptools>=34 diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst index 0eb2186..6108d9b 100644 --- a/docs/userguide/dependency_management.rst +++ b/docs/userguide/dependency_management.rst @@ -49,23 +49,27 @@ be able to run. ``setuptools`` support automatically download and install these dependencies when the package is installed. Although there is more finess to it, let's start with a simple example. -.. code-block:: ini +.. tab:: setup.cfg - [options] - #... - install_requires = - docutils - BazSpam ==1.1 + .. code-block:: ini + + [options] + #... + install_requires = + docutils + BazSpam ==1.1 + +.. tab:: setup.py -.. code-block:: python + .. code-block:: python - setup( - #..., - install_requires = [ - 'docutils', - 'BazSpam ==1.1' - ] - ) + setup( + #..., + install_requires = [ + 'docutils', + 'BazSpam ==1.1' + ] + ) When your project is installed (e.g. using pip), all of the dependencies not @@ -82,41 +86,49 @@ specific dependencies. For example, the ``enum`` package was added in Python 3.4, therefore, package that depends on it can elect to install it only when the Python version is older than 3.4. To accomplish this -.. code-block:: ini - - [options] - #... - install_requires = - enum34;python_version<'3.4' +.. tab:: setup.cfg -.. code-block:: python + .. code-block:: ini - setup( + [options] #... - install_requires=[ - "enum34;python_version<'3.4'",] - ) + install_requires = + enum34;python_version<'3.4' + +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + install_requires=[ + "enum34;python_version<'3.4'",] + ) Similarly, if you also wish to declare ``pywin32`` with a minimal version of 1.0 and only install it if the user is using a Windows operating system: -.. code-block:: ini - - [options] - #... - install_requires = - enum34;python_version<'3.4' - pywin32 >= 1.0;platform_system=='Windows' +.. tab:: setup.cfg -.. code-block:: python + .. code-block:: ini - setup( + [options] #... - install_requires=[ - "enum34;python_version<'3.4'", - "pywin32 >= 1.0;platform_system=='Windows'" - ] - ) + install_requires = + enum34;python_version<'3.4' + pywin32 >= 1.0;platform_system=='Windows' + +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + install_requires=[ + "enum34;python_version<'3.4'", + "pywin32 >= 1.0;platform_system=='Windows'" + ] + ) The environmental markers that may be used for testing platform types are detailed in `PEP 508 `_. @@ -181,20 +193,24 @@ The ``dependency_links`` option takes the form of a list of URL strings. For example, this will cause a search of the specified page for eggs or source distributions, if the package's dependencies aren't already installed: -.. code-block:: ini - - [options] - #... - dependency_links = http://peak.telecommunity.com/snapshots/ +.. tab:: setup.cfg -.. code-block:: python + .. code-block:: ini - setup( + [options] #... - dependency_links=[ - "http://peak.telecommunity.com/snapshots/" - ], - ) + dependency_links = http://peak.telecommunity.com/snapshots/ + +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + dependency_links=[ + "http://peak.telecommunity.com/snapshots/" + ], + ) Optional dependencies @@ -211,24 +227,28 @@ ancillary functions such as "tests" and "docs". For example, Package-A offers optional PDF support and requires two other dependencies for it to work: -.. code-block:: ini +.. tab:: setup.cfg - [metadata] - name = Package-A + .. code-block:: ini - [options.extras_require] - PDF = ReportLab>=1.2; RXP + [metadata] + name = Package-A + [options.extras_require] + PDF = ReportLab>=1.2; RXP -.. code-block:: python - setup( - name="Project-A", - #... - extras_require={ - "PDF": ["ReportLab>=1.2", "RXP"], - } - ) +.. tab:: setup.py + + .. code-block:: python + + setup( + name="Project-A", + #... + extras_require={ + "PDF": ["ReportLab>=1.2", "RXP"], + } + ) The name ``PDF`` is an arbitary identifier of such a list of dependencies, to which other components can refer and have them installed. There are two common @@ -236,31 +256,35 @@ use cases. First is the console_scripts entry point: -.. code-block:: ini +.. tab:: setup.cfg - [metadata] - name = Project A - #... + .. code-block:: ini - [options] - #... - entry_points= - [console_scripts] - rst2pdf = project_a.tools.pdfgen [PDF] - rst2html = project_a.tools.htmlgen - -.. code-block:: python - - setup( - name = "Project-A" - #..., - entry_points={ - "console_scripts": [ - "rst2pdf = project_a.tools.pdfgen [PDF]", - "rst2html = project_a.tools.htmlgen", - ], - } - ) + [metadata] + name = Project A + #... + + [options] + #... + entry_points= + [console_scripts] + rst2pdf = project_a.tools.pdfgen [PDF] + rst2html = project_a.tools.htmlgen + +.. tab:: setup.py + + .. code-block:: python + + setup( + name = "Project-A" + #..., + entry_points={ + "console_scripts": [ + "rst2pdf = project_a.tools.pdfgen [PDF]", + "rst2html = project_a.tools.htmlgen", + ], + } + ) This syntax indicates that the entry point (in this case a console script) is only valid when the PDF extra is installed. It is up to the installer @@ -273,24 +297,28 @@ The second use case is that other package can use this "extra" for their own dependencies. For example, if "Project-B" needs "project A" with PDF support installed, it might declare the dependency like this: -.. code-block:: ini +.. tab:: setup.cfg - [metadata] - name = Project-B - #... + .. code-block:: ini - [options] - #... - install_requires = - Project-A[PDF] + [metadata] + name = Project-B + #... + + [options] + #... + install_requires = + Project-A[PDF] + +.. tab:: setup.py -.. code-block:: python + .. code-block:: python - setup( - name="Project-B", - install_requires=["Project-A[PDF]"], - ... - ) + setup( + name="Project-B", + install_requires=["Project-A[PDF]"], + ... + ) This will cause ReportLab to be installed along with project A, if project B is installed -- even if project A was already installed. In this way, a project diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index de4ef66..842ade8 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -19,36 +19,44 @@ Package Discovery and Namespace Package support for namespace package. Normally, you would specify the package to be included manually in the following manner: -.. code-block:: ini - - [options] - #... - packages = - mypkg1 - mypkg2 +.. tab:: setup.cfg -.. code-block:: python + .. code-block:: ini - setup( + [options] #... - packages = ['mypkg1', 'mypkg2'] - ) + packages = + mypkg1 + mypkg2 + +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + packages = ['mypkg1', 'mypkg2'] + ) This can get tiresome reallly quickly. To speed things up, we introduce two functions provided by setuptools: -.. code-block:: ini +.. tab:: setup.cfg - [options] - packages = find: - #or - packages = find_namespace: + .. code-block:: ini -.. code-block:: python + [options] + packages = find: + #or + packages = find_namespace: - from setuptools import find_packages - #or - from setuptools import find_namespace_packages +.. tab:: setup.py + + .. code-block:: python + + from setuptools import find_packages + #or + from setuptools import find_namespace_packages Using ``find:`` or ``find_packages`` @@ -71,30 +79,34 @@ it, consider the following directory To have your setup.cfg or setup.py to automatically include packages found in ``src`` that starts with the name ``pkg`` and not ``additional``: -.. code-block:: ini +.. tab:: setup.cfg - [options] - packages = find: - package_dir = - =src + .. code-block:: ini - [options.packages.find] - where = src - include = pkg* - exclude = additional + [options] + packages = find: + package_dir = + =src -.. code-block:: python + [options.packages.find] + where = src + include = pkg* + exclude = additional - setup( - #... - packages = find_packages( - where = 'src', - include = ['pkg*',], - exclude = ['additional',] - ), - package_dir = {"":"src"} - #... - ) +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + packages = find_packages( + where = 'src', + include = ['pkg*',], + exclude = ['additional',] + ), + package_dir = {"":"src"} + #... + ) .. _Namespace Packages: @@ -195,17 +207,21 @@ following: And the ``namespace_packages`` keyword in your ``setup.cfg`` or ``setup.py``: -.. code-block:: ini +.. tab:: setup.cfg - [options] - namespace_packages = timmins + .. code-block:: ini -.. code-block:: python + [options] + namespace_packages = timmins + +.. tab:: setup.py + + .. code-block:: python - setup( - # ... - namespace_packages = ['timmins'] - ) + setup( + # ... + namespace_packages = ['timmins'] + ) And your directory should look like this diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index 75dc302..7f111dd 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -37,26 +37,45 @@ package your project: requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" -Then, you will need a ``setup.cfg`` to specify your package information, -such as metadata, contents, dependencies, etc. Here we demonstrate the minimum +Then, you will need a ``setup.cfg`` or ``setup.py`` to specify your package +information, such as metadata, contents, dependencies, etc. Here we demonstrate +the minimum -.. code-block:: ini +.. tab:: setup.cfg - [metadata] - name = mypackage - version = 0.0.1 + .. code-block:: ini - [options] - packages = mypackage - install_requires = - requests - importlib; python_version == "2.6" + [metadata] + name = mypackage + version = 0.0.1 + + [options] + packages = mypackage + install_requires = + requests + importlib; python_version == "2.6" + +.. tab:: setup.py + + .. code-block:: python + + from setuptools import setup + + setup( + name='mypackage"' + version='0.0.1', + packages=['mypackage'], + install_requires=[ + 'requests', + 'importlib; python_version == "2.6"', + ], + ) This is what your project would look like:: ~/mypackage/ pyproject.toml - setup.cfg + setup.cfg # or setup.py mypackage/__init__.py Then, you need an builder, such as :std:doc:`PyPA build ` diff --git a/setup.cfg b/setup.cfg index 1895061..7b05eed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ license_files = LICENSE name = setuptools -version = 54.1.0 +version = 54.1.1 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages @@ -67,6 +67,7 @@ docs = sphinx jaraco.packaging >= 8.2 rst.linker >= 1.9 + sphinx-inline-tabs # local pygments-github-lexers==0.0.5 diff --git a/setuptools/dist.py b/setuptools/dist.py index c074468..70c0e6b 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -599,6 +599,7 @@ class Distribution(_Distribution): val = parser.get(section, opt) opt = self.dash_to_underscore_warning(opt, section) + opt = self.make_option_lowercase(opt, section) opt_dict[opt] = (filename, val) # Make the ConfigParser forget everything (so we retain @@ -636,6 +637,18 @@ class Distribution(_Distribution): % (opt, underscore_opt)) return underscore_opt + def make_option_lowercase(self, opt, section): + if section != 'metadata' or opt.islower(): + return opt + + lowercase_opt = opt.lower() + warnings.warn( + "Usage of uppercase key '%s' in '%s' will be deprecated in future " + "versions. Please use lowercase '%s' instead" + % (opt, section, lowercase_opt) + ) + return lowercase_opt + # FIXME: 'Distribution._set_command_options' is too complex (14) def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 """ diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index eac2674..1ff5ee4 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -526,6 +526,25 @@ class TestMetadata: assert metadata.author_email == 'test@test.com' assert metadata.maintainer_email == 'foo@foo.com' + def test_uppercase_warning(self, tmpdir): + # remove this test and the method uppercase_warning() in setuptools.dist + # when no longer needed + fake_env( + tmpdir, + '[metadata]\n' + 'Name = foo\n' + 'description = Some description\n' + ) + msg = ("Usage of uppercase key 'Name' in 'metadata' will be deprecated in " + "future versions. " + "Please use lowercase 'name' instead") + with pytest.warns(UserWarning, match=msg): + with get_dist(tmpdir) as dist: + metadata = dist.metadata + + assert metadata.name == 'foo' + assert metadata.description == 'Some description' + class TestOptions: -- 2.34.1