diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6c69ceb --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +ignore = E203, E231, E266, E302, E501, W503 +max-line-length = 88 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20adb54..745ee39 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12"] + python-version: ["3.12", "3.13"] os: [macOS-latest, ubuntu-latest, windows-latest] steps: @@ -25,10 +25,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install - run: | - python -m pip install --upgrade pip - make setup - pip install -U . + run: make install - name: Test run: make test - name: Lint diff --git a/Makefile b/Makefile index 9731222..a8bc702 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,27 @@ PYTHON?=python -SOURCES=squatter setup.py +SOURCES=squatter + +UV:=$(shell uv --version) +ifdef UV + VENV:=uv venv + PIP:=uv pip +else + VENV:=python -m venv + PIP:=python -m pip +endif .PHONY: venv venv: - $(PYTHON) -m venv .venv - source .venv/bin/activate && make setup + $(VENV) .venv + source .venv/bin/activate && make install @echo 'run `source .venv/bin/activate` to use virtualenv' # The rest of these are intended to be run within the venv, where python points # to whatever was used to set up the venv. -.PHONY: setup -setup: - python -m pip install -Ur requirements-dev.txt +.PHONY: install +install: + $(PIP) install -Ue .[dev] .PHONY: test test: @@ -27,10 +36,10 @@ format: lint: python -m ufmt check $(SOURCES) python -m flake8 $(SOURCES) - mypy --strict squatter + python -m mypy squatter .PHONY: release release: rm -rf dist - python setup.py sdist bdist_wheel - twine upload dist/* + hatch build + hatch publish diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..eb7df3f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "squatter" +readme = "README.md" +authors = [ + {name="Amethyst Reese", email="amethyst@n7.gg"}, + {name="Tim Hatch"}, +] +license = "MIT" +license-files = ["LICENSE"] +dynamic = ["version", "description"] +requires-python = ">=3.12" +dependencies = [ + "click >= 8", + "hatch >= 1.14", +] + +[project.optional-dependencies] +dev = [ + "black==25.1.0", + "coverage==7.8.0", + "flake8==7.2.0", + "mypy==1.15.0", + "tox==4.26.0", + "ufmt==2.8.0", + "usort==1.0.8", + "volatile==2.1.0", +] + +[project.scripts] +squatter = "squatter.__main__:cli" + +[project.urls] +Home = "https://github.com/python-packaging/squatter" + +[tool.hatch.version] +source = "vcs" + +[tool.mypy] +ignore_missing_imports = true +strict = true + +[tool.tox] +env_list = ["3.12", "3.13"] + +[tool.tox.env_run_base] +commands = [["make", "test"]] +extras = ["dev"] +# set_env = { COVERAGE_FILE="{env:env_dir}/.coverage" } +allowlist_externals = ["make"] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 2a3a4a6..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,10 +0,0 @@ -black==24.3.0 -coverage==7.4.0 -flake8==7.0.0 -mypy==1.8.0 -tox==4.12.0 -twine==4.0.2 -volatile==2.1.0 -wheel==0.42.0 -ufmt==2.3.0 -usort==1.0.7 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 7d3e849..0000000 --- a/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -[metadata] -name = squatter -description = Generates minimal setup.py to register a name on pypi -long_description = file: README.md -long_description_content_type = text/markdown -license = MIT -url = https://github.com/python-packaging/squatter -author = Amethyst Reese -author_email = amy@n7.gg - -[options] -packages = squatter -setup_requires = - setuptools_scm >= 8 - setuptools >= 38.3.0 -python_requires = >=3.11 -install_requires = - setuptools >= 38.3.0 - -[options.entry_points] -console_scripts = - squatter = squatter.__main__:cli - -[check] -metadata = true -strict = true - -[coverage:run] -branch = True -include = squatter/* -omit = squatter/tests/* - -[coverage:report] -fail_under = 100 -precision = 1 -show_missing = True -skip_covered = True - -[mypy] -ignore_missing_imports = True - -[tox:tox] -envlist = py311, py312 - -[testenv] -deps = -rrequirements-dev.txt -whitelist_externals = make -commands = - make test -setenv = - py{311,312}: COVERAGE_FILE={envdir}/.coverage - -[flake8] -ignore = E203, E231, E266, E302, E501, W503 -max-line-length = 88 diff --git a/setup.py b/setup.py deleted file mode 100644 index d5d43d7..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup(use_scm_version=True) diff --git a/squatter/templates.py b/squatter/templates.py index fff9c67..c47ea32 100644 --- a/squatter/templates.py +++ b/squatter/templates.py @@ -1,19 +1,23 @@ -import glob -import sys from pathlib import Path from subprocess import check_call, check_output from typing import Optional -SETUP_PY_TMPL = """\ -from setuptools import setup +PYPROJECT_TEMPLATE = """\ +["build-system"] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = {package_name!r} +description = "coming soon" +version = "0.0.0a1" +authors = [ + {{name={author!r}, email={author_email!r} }}, +] +""" -setup( - name={package_name!r}, - description="coming soon", - version="0.0.0a1", - author={author!r}, - author_email={author_email!r}, -) +INIT_TEMPLATE = """\ +'''coming soon''' """ @@ -36,15 +40,16 @@ def generate( ["git", "config", "user.email"], encoding="utf-8" ).strip() - data = SETUP_PY_TMPL.format(**locals()) - (Path(self.staging_directory) / "setup.py").write_text(data) + data = PYPROJECT_TEMPLATE.format(**locals()) + (Path(self.staging_directory) / "pyproject.toml").write_text(data) + + pkg_dir = Path(self.staging_directory) / package_name.replace("-", "_") + pkg_dir.mkdir(parents=True, exist_ok=True) + (pkg_dir / "__init__.py").write_text(INIT_TEMPLATE) def sdist(self) -> None: - check_call([sys.executable, "setup.py", "sdist"], cwd=self.staging_directory) + check_call(["hatch", "build"], cwd=self.staging_directory) def upload(self) -> None: self.sdist() - check_call( - ["twine", "upload"] + glob.glob(f"{self.staging_directory}/dist/*.tar.gz"), - cwd=self.staging_directory, - ) + check_call(["hatch", "upload"], cwd=self.staging_directory) diff --git a/squatter/tests/__init__.py b/squatter/tests/__init__.py index 06aa9c8..1f1d78f 100644 --- a/squatter/tests/__init__.py +++ b/squatter/tests/__init__.py @@ -1,3 +1,4 @@ +import tarfile import unittest from pathlib import Path from subprocess import check_call @@ -18,17 +19,26 @@ def test_env_smoke(self) -> None: env.generate("foo", "Author Name", "email@example.com") env.sdist() + tarballs = list(Path(d).rglob("*.tar.gz")) self.assertEqual( [Path(d) / "dist" / "foo-0.0.0a1.tar.gz"], - list(Path(d).rglob("*.tar.gz")), + tarballs, ) - egg_info = list(Path(d).rglob("PKG-INFO")) - self.assertEqual(1, len(egg_info)) - egg_info_text = egg_info[0].read_text() - self.assertIn("\nName: foo\n", egg_info_text) - self.assertIn("\nAuthor: Author Name\n", egg_info_text) - self.assertIn("\nAuthor-email: email@example.com\n", egg_info_text) + with tarfile.open(tarballs[0]) as tar: + members = [ + member + for member in tar.getmembers() + if member.name.endswith("PKG-INFO") + ] + pkg_info = tar.extractfile(members[0]) + assert pkg_info is not None + egg_info_text = pkg_info.read() + + self.assertIn(b"\nName: foo\n", egg_info_text) + self.assertIn( + b"\nAuthor-email: Author Name \n", egg_info_text + ) @patch("squatter.templates.check_output") def test_env_git_prompts(self, check_output_mock: Any) -> None: @@ -43,17 +53,26 @@ def test_env_git_prompts(self, check_output_mock: Any) -> None: env.generate("foo") env.sdist() + tarballs = list(Path(d).rglob("*.tar.gz")) self.assertEqual( [Path(d) / "dist" / "foo-0.0.0a1.tar.gz"], - list(Path(d).rglob("*.tar.gz")), + tarballs, ) - egg_info = list(Path(d).rglob("PKG-INFO")) - self.assertEqual(1, len(egg_info)) - egg_info_text = egg_info[0].read_text() - self.assertIn("\nName: foo\n", egg_info_text) - self.assertIn("\nAuthor: Bob\n", egg_info_text) - self.assertIn("\nAuthor-email: email@example.com\n", egg_info_text) + with tarfile.open(tarballs[0]) as tar: + members = [ + member + for member in tar.getmembers() + if member.name.endswith("PKG-INFO") + ] + pkg_info = tar.extractfile(members[0]) + assert pkg_info is not None + egg_info_text = pkg_info.read() + + self.assertIn(b"\nName: foo\n", egg_info_text) + self.assertIn( + b"\nAuthor-email: Bob \n", egg_info_text + ) @patch("squatter.templates.check_output") @patch("squatter.templates.check_call") @@ -68,11 +87,11 @@ def test_cli_functional(self, check_call_mock: Any, check_output_mock: Any) -> N def patched_check_call(cmd: List[str], **kwargs: Any) -> Any: nonlocal uploads - if cmd[0] != "twine": + if cmd[0] != "hatch": return check_call(cmd, **kwargs) else: - assert cmd[-1].endswith(".tar.gz") - uploads += 1 + if "upload" in cmd: + uploads += 1 check_call_mock.side_effect = patched_check_call