From 1bfc1f058e532be882ff46c18990727c329a27da Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 11 Aug 2024 19:45:12 +0100 Subject: [PATCH] Adds pyproject.toml (#77) --- .black | 3 - .editorconfig | 3 + .github/workflows/_tests.yml | 73 ++++++++++++--- README.rst | 4 +- django_squash/db/migrations/utils.py | 2 +- pyproject.toml | 134 +++++++++++++++++++++++++++ setup.cfg | 55 ----------- setup.py | 100 -------------------- tests/utils.py | 86 +++++++++++++++-- 9 files changed, 280 insertions(+), 180 deletions(-) delete mode 100644 .black create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100755 setup.py diff --git a/.black b/.black deleted file mode 100644 index 9a12e65..0000000 --- a/.black +++ /dev/null @@ -1,3 +0,0 @@ -[tool.black] -line-length = 119 -exclude = "venv/" diff --git a/.editorconfig b/.editorconfig index 168b26c..f737b8f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,3 +13,6 @@ charset = utf-8 # Docstrings and comments use max_line_length = 79 [*.py] max_line_length = 119 + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/_tests.yml b/.github/workflows/_tests.yml index 149b8b3..86f494a 100644 --- a/.github/workflows/_tests.yml +++ b/.github/workflows/_tests.yml @@ -26,19 +26,65 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} - name: Install the linters run: | - pip install --upgrade setuptools - python setup.py install_linters - - name: Generate matrix from setup.py - id: set-matrix + pip install --upgrade setuptools toml + pip install $(python -c 'import toml; conf = toml.load(open("pyproject.toml")); print(" ".join(conf["project"]["optional-dependencies"]["lint"]))') + - name: Generate matrix run: | - python -c "from setup import GITHUB_MATRIX; print(GITHUB_MATRIX)" > matrix.json - echo "matrix=$(> $GITHUB_OUTPUT + python -c ' + import toml + import itertools + import json + import os + + DJANGO_VERSIONS = [] + PYTHON_VERSIONS = [] + EXCLUDE_MATRIX = (["3.8", "3.9"], ["5.0.*", "5.1.*", "main"]) + + def is_number(s): + try: + float(s) + return True + except ValueError: + return False + + with open("pyproject.toml") as f: + conf = toml.load(f) + for classifier in conf["project"]["classifiers"]: + if "Framework :: Django ::" in classifier: + version = classifier.split("::")[-1].strip() + if "." in version and is_number(version): + DJANGO_VERSIONS.append(version) + elif "Programming Language :: Python ::" in classifier: + version = classifier.split("::")[-1].strip() + if "." in version and is_number(version): + PYTHON_VERSIONS.append(version) + + matrix = { + "python-version": PYTHON_VERSIONS, + "django-version": [f"{v}.*" for v in DJANGO_VERSIONS] + ["main"], + "exclude": [{"django-version": d, "python-version": p} for p, d in itertools.product(*EXCLUDE_MATRIX)], + } + + with open(os.getenv("GITHUB_ENV"), "a") as env_file: + pretty = " ".join(DJANGO_VERSIONS) + env_file.write(f"django={pretty}\n") + pretty = " ".join(PYTHON_VERSIONS) + env_file.write(f"python={pretty}\n") + env_file.write(f"matrix={json.dumps(matrix)}\n") + ' - name: Check version EOF + id: set-matrix run: | - python -c "import json + echo "matrix=$matrix" >> $GITHUB_OUTPUT + python -c " + import os + import json from urllib.request import Request, urlopen from datetime import date, datetime, timedelta - from setup import DJANGO_VERSIONS, PYTHON_VERSIONS + + DJANGO_VERSIONS = os.getenv('django').split() + PYTHON_VERSIONS = os.getenv('python').split() + today = date.today() WARNING_DAYS = timedelta(days=90) version_by_product = { @@ -47,8 +93,8 @@ jobs: } for product, supported_versions in version_by_product.items(): url = f'https://endoflife.date/api/{product}.json' - with urlopen(Request(url)) as httpresponse: - data = json.loads(httpresponse.read()) + with urlopen(Request(url)) as response: + data = json.loads(response.read()) for detail in data: version = detail['cycle'] eol = detail['eol'] @@ -60,13 +106,14 @@ jobs: if eol_date < today: print(f'::error ::{product} v{version}: EOL was {eol}') elif eol_date - today < WARNING_DAYS: - print(f'::warning ::{product} v{version}: EOL is coming up on the {eol}')" + print(f'::warning ::{product} v{version}: EOL is coming up on the {eol}') + " - name: iSort check if: always() run: isort --check . - name: Black check if: always() - run: black --config .black --check . + run: black --check . - name: Flake8 check if: always() run: flake8 . @@ -136,7 +183,7 @@ jobs: - name: Clean up temporary artifacts uses: geekyeggo/delete-artifact@v5 with: - name: coverage_* + name: coverage_* - name: Combine coverage.py run: | coverage combine $(find downloaded_artifacts/ -type f | xargs) diff --git a/README.rst b/README.rst index 1279970..5fbd1c9 100644 --- a/README.rst +++ b/README.rst @@ -60,7 +60,7 @@ Developing .. code-block:: shell - docker run --rm -it -v .:/app -v django-squash-pip-cache:/root/.cache/pip -e PYTHONDONTWRITEBYTECODE=1 python:3.12 bash -c "cd app; pip install -e .[test,lint]; echo \"alias linters=\\\"echo '> isort'; isort .; echo '> black'; black --config .black .; echo '> ruff'; ruff check .;echo '> flake8'; flake8 .; echo '> rst-lint'; rst-lint README.rst docs/*\\\"\" >> ~/.bash_profile; printf '\n\n\nrun **pytest** to run tests, **linters** to run linters\n\n'; exec bash --init-file ~/.bash_profile" + docker run --rm -it -v .:/app -v django-squash-pip-cache:/root/.cache/pip -e PYTHONDONTWRITEBYTECODE=1 python:3.12 bash -c "cd app; pip install -e .[test,lint]; echo \"alias linters=\\\"echo '> isort'; isort .; echo '> black'; black .; echo '> ruff'; ruff check .;echo '> flake8'; flake8 .; echo '> rst-lint'; rst-lint README.rst docs/*\\\"\" >> ~/.bash_profile; printf '\n\n\nrun **pytest** to run tests, **linters** to run linters\n\n'; exec bash --init-file ~/.bash_profile" Alternatively, you can also create a virtual environment and run @@ -87,7 +87,7 @@ Alternatively, you can also create a virtual environment and run .. code-block:: shell isort . - black --config .black . + black . flake8 . ruff check . rst-lint . diff --git a/django_squash/db/migrations/utils.py b/django_squash/db/migrations/utils.py index c0eeb31..1279c00 100644 --- a/django_squash/db/migrations/utils.py +++ b/django_squash/db/migrations/utils.py @@ -167,9 +167,9 @@ def is_code_in_site_packages(module_name): site_packages_path_ = site_packages_path() try: loader = importlib.util.find_spec(module_name) - return site_packages_path_ in loader.origin except ImportError: return False + return loader.origin.startswith(site_packages_path_) @functools.lru_cache(maxsize=1) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..757cacb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,134 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "django_squash" +version = "0.0.11" +description = "A migration squasher that doesn't care how Humpty Dumpty was put together." +readme = "README.rst" +keywords = ["django", "migration", "squashing", "squash"] +authors = [ + {name = "Javier Buzzi", email = "buzzi.javier@gmail.com"}, +] +license = {text = "MIT"} +classifiers = [ + # See https://pypi.org/pypi?%3Aaction=list_classifiers + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", + "License :: OSI Approved :: MIT License", +] +dependencies = [ + "django >=3.2", +] +requires-python = ">=3.8" + +[project.optional-dependencies] +lint = [ + "black", + "flake8-pyproject", + "flake8-tidy-imports", + "isort", + "pygments", + "restructuredtext-lint", + "ruff" +] +test = [ + "black", + "build", + "ipdb", + "libcst", + "psycopg2-binary", + "pytest-cov", + "pytest-django" +] + +[project.urls] +homepage = "https://github.com/kingbuzzman/django-squash" + +[tool.setuptools.packages.find] +exclude = ["tests*", "docs*"] + +[tool.setuptools] +zip-safe = true +platforms = ["any"] + +[tool.black] +line-length = 119 +exclude = "venv/" + +[tool.flake8] +max-line-length = 119 +exclude = ["*/migrations_*/*", "venv/*", "build/*"] +ban-relative-imports = true + +[tool.isort] +combine_as_imports = true +line_length = 119 +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +skip_glob = ["venv/**"] + +[tool.coverage.run] +source = ["django_squash"] + +[tool.coverage.report] +omit = ["*/migrations_*/*"] +show_missing = true +fail_under = 95 + +[tool.pypi] +repository = "https://upload.pypi.org/legacy/" +username = "kingbuzzman" + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "settings" +pythonpath = "tests" +addopts = "--pdbcls=IPython.terminal.debugger:TerminalPdb" +python_files = ["test_*.py", "*_tests.py"] + +# Custom markers for pytest +markers = [ + "temporary_migration_module", + "temporary_migration_module2", + "temporary_migration_module3", + "slow: marks tests as slow" +] + +filterwarnings = [ + "error", + + # Internal warning to tell the user that the writer.py file has changed, and may not be compatible. + "ignore:Django migrations writer file has changed and may not be compatible with django-squash", + + # Warning: django.utils.deprecation.RemovedInDjango50Warning: The USE_L10N setting is deprecated. Starting with Django 5.0, localized formatting of data will always be enabled. For example Django will display numbers and dates using the format of the current locale. + # Don't specify the exact warning (django.utils.deprecation.RemovedInDjango50Warning) as not all version of Django know it and pytest will fail + "ignore:The USE_L10N setting is deprecated:", + + # Warning: cgi is only being used by Django 3.2 + "ignore:'cgi' is deprecated and slated for removal in Python 3.13", + + # Django 3.2 throws a warning about the USE_I18N setting being deprecated + "ignore:datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects .*" +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b94eab8..0000000 --- a/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -[flake8] -max-line-length = 119 -exclude = */migrations_*/*,venv/*,build/* -ban-relative-imports = true - -[tool:isort] -combine_as_imports = true -line_length = 119 -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -skip_glob = venv/** - -[coverage:run] -source=django_squash - -[coverage:report] -omit=*/migrations_*/* -show_missing=true -fail_under=95 - -[distutils] -index-servers=pypi - -[pypi] -repository=https://upload.pypi.org/legacy/ -username=kingbuzzman - -[tool:pytest] -DJANGO_SETTINGS_MODULE=settings -pythonpath=tests -addopts="--pdbcls=IPython.terminal.debugger:TerminalPdb" -python_files=test_*.py *_tests.py -markers= - temporary_migration_module - temporary_migration_module2 - temporary_migration_module3 - slow: marks tests as slow -filterwarnings= - error - - # Internal warning to tell the user that the writer.py file has changed, and may not be compatible. - ignore:Django migrations writer file has changed and may not be compatible with django-squash - - # Warning: django.utils.deprecation.RemovedInDjango50Warning: The USE_L10N setting is deprecated. Starting with Django 5.0, localized formatting of data will always be enabled. For example Django will display numbers and dates using the format of the current locale. - # Don't specify the exact warning (django.utils.deprecation.RemovedInDjango50Warning) as not all version of Django know it and pytest will fail - ignore:The USE_L10N setting is deprecated: - - # Warning: cgi is only being used by Django 3.2 - ignore:'cgi' is deprecated and slated for removal in Python 3.13 - - # Django 3.2 throws a warning about the USE_I18N setting being deprecated - ignore:datetime.datetime.utcnow\(\) is deprecated and scheduled for removal in a future version. Use timezone-aware objects .* diff --git a/setup.py b/setup.py deleted file mode 100755 index b4f4a12..0000000 --- a/setup.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python - -import io -import itertools -import json -import os - -here = os.path.abspath(os.path.dirname(__file__)) -with io.open(os.path.join(here, "README.rst"), encoding="utf-8") as fp: - README = fp.read() - -DJANGO_VERSIONS = ["3.2", "4.1", "4.2", "5.0"] # "main" is fictitiously here -PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"] -MIN_DJANGO_VERSION = ".".join(map(str, min([tuple(map(int, v.split("."))) for v in DJANGO_VERSIONS]))) -MIN_PYTHON_VERSION = ".".join(map(str, min([tuple(map(int, v.split("."))) for v in PYTHON_VERSIONS]))) -# Python/Django exceptions -EXCLUDE_MATRIX = (["3.8", "3.9"], ["5.0.*", "main"]) -GITHUB_MATRIX = json.dumps( - { - "python-version": PYTHON_VERSIONS, - "django-version": [f"{v}.*" for v in DJANGO_VERSIONS] + ["main"], - "exclude": [{"django-version": d, "python-version": p} for p, d in itertools.product(*EXCLUDE_MATRIX)], - } -) - -if __name__ == "__main__": - from setuptools import find_packages, setup - from setuptools.command.build_py import build_py as Command - - extras_require = { - "lint": [ - "black", - "flake8", - "flake8-tidy-imports", - "isort", - "pygments", - "restructuredtext-lint", - "ruff", - ], - "test": [ - "black", # tests need black also. - "build", - "ipdb", - "libcst", - "psycopg2-binary", - "pytest-cov", - "pytest-django", - ], - } - - class InstallLintersCommand(Command): - """Custom build command to install ONLY the liners.""" - - def run(self): - req = " ".join(extras_require["lint"]) - os.system(f"pip install {req}") - - setup( - name="django_squash", - version="0.0.11", - description="A migration squasher that doesn't care how Humpty Dumpty was put together.", - long_description=README, - cmdclass={ - "install_linters": InstallLintersCommand, - }, - classifiers=[ - # See https://pypi.org/pypi?%3Aaction=list_classifiers - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Framework :: Django", - ] - + [f"Framework :: Django :: {v}" for v in DJANGO_VERSIONS] - + [ - "Programming Language :: Python", - "Programming Language :: Python :: 3", - ] - + [f"Programming Language :: Python :: {v}" for v in PYTHON_VERSIONS] - + [ - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Utilities", - "License :: OSI Approved :: MIT License", - ], - keywords="django migration squashing squash", - author="Javier Buzzi", - author_email="buzzi.javier@gmail.com", - url="https://github.com/kingbuzzman/django-squash", - license="MIT", - packages=find_packages(exclude=["tests*", "docs*"]), - platforms=["any"], - zip_safe=True, - python_requires=f">={MIN_PYTHON_VERSION}", - install_requires=[ - f"django>={MIN_DJANGO_VERSION}", - ], - tests_require=[], - extras_require=extras_require, - ) diff --git a/tests/utils.py b/tests/utils.py index 3c8c221..d4a3077 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,24 +7,98 @@ import black import libcst -spec = importlib.util.spec_from_file_location("dj_squash_setup", Path().resolve() / "setup.py") -module = importlib.util.module_from_spec(spec) -sys.modules[spec.name] = module -spec.loader.exec_module(module) +try: + import tomllib + + with open("pyproject.toml", "r") as f: # pragma: no cover + if "Programming Language :: Python :: 3.10" not in f.read(): + raise Exception("Delete this try/except block and leave the just the 'import tomllib'.") +except ImportError: + # Python 3.10 does not support tomllib + import tomli as tomllib + +import warnings + +from django import VERSION as _DJANGO_FULL_VERSION +from packaging.version import Version def is_pyvsupported(version): """ Check if the Python version is supported by the package. """ - return version in module.PYTHON_VERSIONS + return Version(version) in SUPPORTED_PYTHON_VERSIONS def is_djvsupported(version): """ Check if the Django version is supported by the package. """ - return version in module.DJANGO_VERSIONS + return Version(version) in SUPPORTED_DJANGO_VERSIONS + + +def is_number(s): + """Returns True if string is a number.""" + try: + float(s) + return True + except ValueError: + return False + + +def shorten_version(version): + parts = version.split(".") + return ".".join(parts[:2]) + + +def is_supported_version(supported_versions, version_to_check): + if version_to_check.is_prerelease: + return True + + for base_version in supported_versions: + if version_to_check.major == base_version.major and version_to_check.minor == base_version.minor: + return True + + return False + + +SUPPORTED_PYTHON_VERSIONS = [] +SUPPORTED_DJANGO_VERSIONS = [] + +with open(Path().resolve() / "pyproject.toml", "rb") as f: + conf = tomllib.load(f) +for classifier in conf["project"]["classifiers"]: + if "Framework :: Django ::" in classifier: + version = classifier.split("::")[-1].strip() + if is_number(version): + SUPPORTED_DJANGO_VERSIONS.append(Version(version)) + globals()["DJ" + version.replace(".", "")] = False + elif "Programming Language :: Python ::" in classifier: + version = classifier.split("::")[-1].strip() + if is_number(version) and "." in version: + SUPPORTED_PYTHON_VERSIONS.append(Version(version)) + globals()["PY" + version.replace(".", "")] = False + +current_python_version = Version(f"{sys.version_info.major}.{sys.version_info.minor}") +pre_release_map = {"alpha": "a", "beta": "b", "rc": "rc"} +# Extract the components of the tuple +major, minor, micro, pre_release, pre_release_num = _DJANGO_FULL_VERSION +# Get the corresponding identifier +pre_release_identifier = pre_release_map.get(pre_release, "") +# Construct the version string +_DJANGO_VERSION = f"{major}.{minor}.{micro}{pre_release_identifier}.{pre_release_num}" +current_django_version = Version(_DJANGO_VERSION) + +globals()["DJ" + shorten_version(str(current_django_version).replace(".", ""))] = True +globals()["PY" + shorten_version(str(current_python_version).replace(".", ""))] = True + +if not is_supported_version(SUPPORTED_DJANGO_VERSIONS, current_django_version): + versions = ", ".join([str(v) for v in SUPPORTED_DJANGO_VERSIONS]) + warnings.warn(f"Current Django version {current_django_version} is not in" f" the supported versions: {versions}") + +if not is_supported_version(SUPPORTED_PYTHON_VERSIONS, current_python_version): + versions = ", ".join([str(v) for v in SUPPORTED_PYTHON_VERSIONS]) + warnings.warn(f"Current Python version {current_python_version} is not in" f" the supported versions: {versions}") def load_migration_module(path):