Repository: twisted/towncrier

Issue:
Switch to hatchling?
I'm currently three layers[^lay] deep yak-shaving, but given that:

1. [pip / setuptools interaction is broken on 3.12](https://github.com/pypa/setuptools/issues/3661)
2. [Twisted has switched to hatchling](https://github.com/twisted/twisted/blob/trunk/pyproject.toml)

Would ya'll be interested to move towncrier to a static config too? Given the simplicity of our setup.py, it should be a simple switcheroo.


[^lay]: I wanted to simplify our CI according to https://hynek.me/articles/why-i-like-nox/, but we have no pre-release so I wanted to add a pre-release, but just adding 3.12 to the noxfile explodes as explained in 1.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/twisted/towncrier/pull/472?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted) Report
Base: **100.00**% // Head: **100.00**% // No change to project coverage :thumbsup:
> Coverage data is based on head [(`f968cb0`)](https://codecov.io/gh/twisted/towncrier/pull/472?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted) compared to base [(`ffe38a1`)](https://codecov.io/gh/twisted/towncrier/commit/ffe38a130a4f9a74516c3920588ae081eb4f0819?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##             trunk      #472   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           13        13           
  Lines          613       614    +1     
  Branches       143       143           
=========================================
+ Hits           613       614    +1     
```


| [Impacted Files](https://codecov.io/gh/twisted/towncrier/pull/472?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted) | Coverage Δ | |
|---|---|---|
| [src/towncrier/\_version.py](https://codecov.io/gh/twisted/towncrier/pull/472?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted#diff-c3JjL3Rvd25jcmllci9fdmVyc2lvbi5weQ==) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/twisted/towncrier/pull/472?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted).

hynek: Source for the rename: https://docs.readthedocs.io/en/stable/config-file/index.html
hynek: > Is check_manifest a false positive?

No, I just forgot to remove it. I'll go thru ci.yml again once this is merged.
hynek: @adiroiban I have just pushed a special gift for you!

Since we're switching to modern static packaging, I have deprecated `towncrier.__version__` altogether.

I don't think we have to wait for a year to remove it, but in the worst case we can.

---

The two warnings it now raises are nonsense (looks like unittest is iterating over all globals!?) I'm not sure how to get rid of it in trial?

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7c85894..c58a121 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -160,9 +160,6 @@ jobs:
           - name: Check Newsfragment
             nox: check_newsfragment
             run-if: ${{ github.head_ref != 'pre-commit-ci-update-config' }}
-          - name: Check package manifest
-            nox: check_manifest
-            run-if: true
           - name: Check mypy
             nox: typecheck
             run-if: true
diff --git a/.gitignore b/.gitignore
index c44127a..fa4a8cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,25 +1,28 @@
-*.egg-info/
+*.lock
 *.o
 *.py[co]
+*.pyproj
 *.so
-_trial_temp*/
-build/
-dropin.cache
-doc/
-docs/_build/
-dist/
-venv/
-htmlcov/
-.coverage
 *~
-*.lock
-apidocs/
-.vs/
-*.pyproj
 .DS_Store
-.eggs
-.nox/
+.coverage
 .coverage.*
-.vscode
+.direnv
+.envrc
 .idea
+.mypy_cache
+.nox/
+.pytest_cache
 .python-version
+.vs/
+.vscode
+Justfile
+_trial_temp*/
+apidocs/
+dist/
+doc/
+docs/_build/
+dropin.cache
+htmlcov/
+tmp/
+venv/
diff --git a/.readthedocs.yml b/.readthedocs.yaml
similarity index 100%
rename from .readthedocs.yml
rename to .readthedocs.yaml
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index e69f970..661f22e 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -128,7 +128,7 @@ The following list contains some ways how to run the test suite:
 * To run some quality checks before you create the pull request,
   we recommend using this call::
 
-    $ nox -e pre_commit check_manifest check_newsfragment
+    $ nox -e pre_commit check_newsfragment
 
 * Or enable `pre-commit` as a git hook::
 
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 9494555..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,20 +0,0 @@
-include *.rst
-include .coveragerc
-include LICENSE
-include CODE_OF_CONDUCT.md
-include pyproject.toml
-include noxfile.py
-include *.yaml
-include .git-blame-ignore-revs
-include .flake8
-recursive-include src *.rst
-
-exclude bin
-exclude admin
-exclude src/towncrier/newsfragments
-exclude .readthedocs.yml
-
-recursive-exclude bin *
-recursive-exclude admin *
-recursive-exclude src/towncrier/newsfragments *
-recursive-exclude docs *
diff --git a/noxfile.py b/noxfile.py
index 38b65b9..607f89f 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -55,12 +55,6 @@ def check_newsfragment(session: nox.Session) -> None:
     session.run("python", "-m", "towncrier.check", "--compare-with", "origin/trunk")
 
 
-@nox.session
-def check_manifest(session: nox.Session) -> None:
-    session.install("check-manifest")
-    session.run("check-manifest")
-
-
 @nox.session
 def typecheck(session: nox.Session) -> None:
     session.install(".", "mypy", "types-setuptools")
@@ -87,11 +81,10 @@ def docs(session: nox.Session) -> None:
 
 @nox.session
 def build(session: nox.Session) -> None:
-    session.install("build", "check-manifest>=0.44", "twine")
+    session.install("build", "twine")
 
-    session.run("check-manifest", "--verbose")
     # If no argument is passed, build builds an sdist and then a wheel from
     # that sdist.
     session.run("python", "-m", "build")
 
-    session.run("twine", "check", "dist/*")
+    session.run("twine", "check", "--strict", "dist/*")
diff --git a/pyproject.toml b/pyproject.toml
index c2145ba..32a2c3c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,80 @@
+[build-system]
+requires = [
+    "hatchling ~= 1.12.2",
+    "wheel ~= 0.38.4",
+    "incremental == 22.10.0",
+]
+build-backend = "hatchling.build"
+
+
+[project]
+dynamic = ["version"]
+name = "towncrier"
+description = "Building newsfiles for your project."
+readme = "README.rst"
+license = "MIT"
+classifiers = [
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: POSIX :: Linux",
+    "Operating System :: MacOS :: MacOS X",
+    "Operating System :: Microsoft :: Windows",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: Implementation :: CPython",
+    "Programming Language :: Python :: Implementation :: PyPy",
+]
+requires-python = ">=3.7"
+dependencies = [
+    "click",
+    "click-default-group",
+    "incremental",
+    "jinja2",
+    "setuptools",
+    "tomli; python_version<'3.11'",
+]
+
+[project.optional-dependencies]
+dev = [
+    "packaging",
+    "sphinx >= 5",
+    "furo",
+    "twisted",
+]
+
+[project.scripts]
+towncrier = "towncrier._shell:cli"
+
+[project.urls]
+Documentation = "https://towncrier.readthedocs.io/"
+Chat = "https://web.libera.chat/?channels=%23twisted"
+"Mailing list" = "https://mail.python.org/mailman3/lists/twisted.python.org/"
+Issues = "https://github.com/twisted/towncrier/issues"
+Repository = "https://github.com/twisted/towncrier"
+Tests = "https://github.com/twisted/towncrier/actions?query=branch%3Atrunk"
+Coverage = "https://codecov.io/gh/twisted/towncrier"
+Distribution = "https://pypi.org/project/towncrier"
+
+
+[tool.hatch.version]
+source = "code"
+path = "src/towncrier/_version.py"
+expression = "_hatchling_version"
+
+[tool.hatch.build]
+exclude = [
+    "admin",
+    "bin",
+    "docs",
+    ".readthedocs.yaml",
+    "src/towncrier/newsfragments",
+]
+
+
 [tool.towncrier]
     package = "towncrier"
     package_dir = "src"
@@ -77,14 +154,6 @@ module = 'incremental'
 ignore_missing_imports = true
 
 
-[build-system]
-requires = [
-    "setuptools ~= 44.1.1",
-    "wheel ~= 0.36.2",
-    "incremental == 22.10.0",
-]
-build-backend = "setuptools.build_meta"
-
 [tool.coverage.run]
 parallel = true
 branch = true
diff --git a/setup.py b/setup.py
deleted file mode 100644
index c6c1169..0000000
--- a/setup.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python
-
-
-# If incremental is not present then setuptools just silently uses v0.0.0 so
-# let's import it and fail instead.
-import incremental  # noqa
-
-from setuptools import find_packages, setup
-
-
-setup(
-    name="towncrier",
-    url="https://github.com/twisted/towncrier",
-    project_urls={
-        "Documentation": "https://towncrier.readthedocs.io/",
-        "Chat": "https://web.libera.chat/?channels=%23twisted",
-        "Mailing list": "https://mail.python.org/mailman3/lists/twisted.python.org/",
-        "Issues": "https://github.com/twisted/towncrier/issues",
-        "Repository": "https://github.com/twisted/towncrier",
-        "Tests": "https://github.com/twisted/towncrier/actions?query=branch%3Atrunk",
-        "Coverage": "https://codecov.io/gh/twisted/towncrier",
-        "Distribution": "https://pypi.org/project/towncrier",
-    },
-    classifiers=[
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: POSIX :: Linux",
-        "Operating System :: MacOS :: MacOS X",
-        "Operating System :: Microsoft :: Windows",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: Implementation :: CPython",
-        "Programming Language :: Python :: Implementation :: PyPy",
-    ],
-    use_incremental=True,
-    python_requires=">=3.7",
-    install_requires=[
-        "click",
-        "click-default-group",
-        "incremental",
-        "jinja2",
-        "setuptools",
-        "tomli; python_version<'3.11'",
-    ],
-    extras_require={
-        "dev": [
-            "packaging",
-            "sphinx >= 5",
-            "furo",
-            "twisted",
-        ],
-    },
-    package_dir={"": "src"},
-    packages=find_packages("src"),
-    license="MIT",
-    zip_safe=False,
-    include_package_data=True,
-    description="Building newsfiles for your project.",
-    long_description=open("README.rst").read(),
-    long_description_content_type="text/x-rst",
-    entry_points={"console_scripts": ["towncrier = towncrier._shell:cli"]},
-)
diff --git a/src/towncrier/__init__.py b/src/towncrier/__init__.py
index ab8ced7..01448b2 100644
--- a/src/towncrier/__init__.py
+++ b/src/towncrier/__init__.py
@@ -7,7 +7,27 @@ towncrier, a builder for your news files.
 
 from __future__ import annotations
 
-from ._version import __version__
+from incremental import Version
 
 
 __all__ = ["__version__"]
+
+
+def __getattr__(name: str) -> Version:
+
+    if name != "__version__":
+        raise AttributeError(f"module {__name__} has no attribute {name}")
+
+    import warnings
+
+    from ._version import __version__
+
+    warnings.warn(
+        "Accessing towncrier.__version__ is deprecated and will be "
+        "removed in a future release. Use importlib.metadata directly "
+        "to query for towncrier's packaging metadata.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+
+    return __version__
diff --git a/src/towncrier/_version.py b/src/towncrier/_version.py
index 40c39c2..a2529bf 100644
--- a/src/towncrier/_version.py
+++ b/src/towncrier/_version.py
@@ -9,4 +9,8 @@ from incremental import Version
 
 
 __version__ = Version("towncrier", 22, 12, 1, dev=0)
-__all__ = ["__version__"]
+# The version is exposed in string format to be
+# available for the hatching build tools.
+_hatchling_version = __version__.short()
+
+__all__ = ["__version__", "_hatchling_version"]


Test patch:
diff --git a/src/towncrier/newsfragments/472.misc b/src/towncrier/newsfragments/472.misc
new file mode 100644
index 0000000..e69de29
diff --git a/src/towncrier/test/test_packaging.py b/src/towncrier/test/test_packaging.py
new file mode 100644
index 0000000..3355ad5
--- /dev/null
+++ b/src/towncrier/test/test_packaging.py
@@ -0,0 +1,22 @@
+# Copyright (c) Amber Brown, 2015
+# See LICENSE for details.
+
+from incremental import Version
+from twisted.trial.unittest import TestCase
+
+from towncrier._version import _hatchling_version
+
+
+class TestPackaging(TestCase):
+    def test_version_warning(self):
+        """
+        Import __version__ from towncrier returns an Incremental version object
+        and raises a warning.
+        """
+        with self.assertWarnsRegex(
+            DeprecationWarning, "Accessing towncrier.__version__ is deprecated.*"
+        ):
+            from towncrier import __version__
+
+        self.assertIsInstance(__version__, Version)
+        self.assertEqual(_hatchling_version, __version__.short())
diff --git a/src/towncrier/test/test_project.py b/src/towncrier/test/test_project.py
index e55a309..4b42064 100644
--- a/src/towncrier/test/test_project.py
+++ b/src/towncrier/test/test_project.py
@@ -55,8 +55,18 @@ class VersionFetchingTests(TestCase):
         """
         pkg = "../src"
 
-        self.assertEqual(metadata_version("towncrier"), get_version(pkg, "towncrier"))
-        self.assertEqual("towncrier", get_project_name(pkg, "towncrier"))
+        with self.assertWarnsRegex(
+            DeprecationWarning, "Accessing towncrier.__version__ is deprecated.*"
+        ):
+            version = get_version(pkg, "towncrier")
+
+        with self.assertWarnsRegex(
+            DeprecationWarning, "Accessing towncrier.__version__ is deprecated.*"
+        ):
+            name = get_project_name(pkg, "towncrier")
+
+        self.assertEqual(metadata_version("towncrier"), version)
+        self.assertEqual("towncrier", name)
 
     def _setup_missing(self):
         """