diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a145395..dcd77df 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -135,7 +135,7 @@ Before you submit a pull request, check that it meets these guidelines: your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 2.7, 3.5, 3.6 and 3.7, and for PyPy. Check +3. The pull request should work for Python 3.8, 3.9, 3.10, 3.11 and 3.12, and for PyPy. Check https://travis-ci.org/audreyr/cookiecutter-pypackage/pull_requests and make sure that the tests pass for all supported Python versions. diff --git a/README.rst b/README.rst index f19945f..07db73d 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ Features * Testing setup with ``unittest`` and ``pytest`` * Github-Actions_: Ready for Github Actions Continuous Integration testing * Travis-CI_: Ready for Travis Continuous Integration testing -* Tox_ testing: Setup to easily test for Python 2.7, 3.5, 3.6, 3.7, 3.8 +* Tox_ testing: Setup to easily test for Python 3.8, 3.9, 3.10, 3.11, 3.12 * Sphinx_ docs: Documentation ready for generation, automatic building and deploying to gh-pages (strongly recommended) * Bumpversion_: Pre-configured version bumping with a single command * Auto-release to PyPI_ when you push a new tag to master (strongly recommended) @@ -199,8 +199,8 @@ things are files and folders and I'll give you a quick overview of what they are/do. * Tox_ (tox.ini): A system that can run all kinds of tests for you. For - instance, you can test your code on various versions (Python 2.7, 3.5, - 3.6, 3.7, 3.8) and test your code on linters as well. + instance, you can test your code on various versions (Python 3.8, + 3.9, 3.10, 3.11, 3.12) and test your code on linters as well. * Travis-CI_ (travis.yml): A continuous integration system. That means every time you push a commit it will simulate downloading your project, installing diff --git a/cookiecutter.json b/cookiecutter.json index 68ca7c5..05c256a 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -2,7 +2,7 @@ "full_name": "Your Name", "email": "email@mit.edu", "github_username": "username", - "github_owner": ["DAI-Lab", "HDI-Project", "D3-AI", "data-dev", "sdv-dev", "{{ cookiecutter.github_username }}"], + "github_owner": ["DAI-Lab", "HDI-Project", "sintel-dev", "data-dev", "{{ cookiecutter.github_username }}"], "project_name": "Python Boilerplate", "package_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "project_slug": "{{ cookiecutter.package_name.replace('-', '_') }}", @@ -12,7 +12,6 @@ "license_owner_email": "{{ 'dailabmit@gmail.com' if cookiecutter.github_username != cookiecutter.github_owner else cookiecutter.email }}", "pypi_username": "{{ 'mit_dai_lab' if cookiecutter.github_username != cookiecutter.github_owner else cookiecutter.github_username }}", "version": "0.1.0.dev0", - "support_py2": "n", "ci_provider": ["Github Actions", "Travis CI", "No continuous integration"], "use_pypi_with_ci": "n", "use_ghpages_with_ci": "y", diff --git a/{{cookiecutter.repository_name}}/.github/workflows/deploy.yml b/{{cookiecutter.repository_name}}/.github/workflows/deploy.yml index 42e5abc..bb09311 100644 --- a/{{cookiecutter.repository_name}}/.github/workflows/deploy.yml +++ b/{{cookiecutter.repository_name}}/.github/workflows/deploy.yml @@ -16,10 +16,10 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.8 - name: Install dependencies run: | diff --git a/{{cookiecutter.repository_name}}/.github/workflows/docs.yml b/{{cookiecutter.repository_name}}/.github/workflows/docs.yml index b11e9d7..b9f8856 100644 --- a/{{cookiecutter.repository_name}}/.github/workflows/docs.yml +++ b/{{cookiecutter.repository_name}}/.github/workflows/docs.yml @@ -14,7 +14,7 @@ jobs: - name: Python uses: actions/setup-python@v1 with: - python-version: '3.7' + python-version: '3.8' - name: Build run: | diff --git a/{{cookiecutter.repository_name}}/.github/workflows/tests.yml b/{{cookiecutter.repository_name}}/.github/workflows/tests.yml index 9ee10bf..7a97372 100644 --- a/{{cookiecutter.repository_name}}/.github/workflows/tests.yml +++ b/{{cookiecutter.repository_name}}/.github/workflows/tests.yml @@ -1,3 +1,6 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + name: Run Tests on: @@ -7,32 +10,117 @@ on: branches: [ master ] jobs: - build: + lint: + runs-on: {{ '${{ matrix.os }}' }} + strategy: + matrix: + python-version: [3.8] + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v1 + - name: Set up Python {{ '${{ matrix.python-version }}' }} + uses: actions/setup-python@v2 + with: + python-version: {{ '${{ matrix.python-version }}' }} + - name: Install package + run: pip install invoke .[dev] + - name: invoke lint + run: invoke lint + + + docs: runs-on: {{ '${{ matrix.os }}' }} strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v1 - name: Set up Python {{ '${{ matrix.python-version }}' }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: {{ '${{ matrix.python-version }}' }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions + - name: Install package + run: pip install .[dev] + - name: make docs + run: make docs - - name: Test with tox - run: tox -{%- if cookiecutter.use_codecov_with_ci == 'y' %} + readme: + runs-on: {{ '${{ matrix.os }}' }} + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v1 + - name: Set up Python {{ '${{ matrix.python-version }}' }} + uses: actions/setup-python@v2 + with: + python-version: {{ '${{ matrix.python-version }}' }} + - name: Install package and dependencies + run: pip install invoke rundoc . + - name: invoke readme + run: invoke readme + + unit: + runs-on: {{ '${{ matrix.os }}' }} + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v1 + - name: Set up Python {{ '${{ matrix.python-version }}' }} + uses: actions/setup-python@v2 + with: + python-version: {{ '${{ matrix.python-version }}' }} + - name: Install package and dependencies + run: pip install invoke .[test] + - name: invoke unit + run: invoke unit + +{%- if cookiecutter.use_codecov_with_ci == 'y' %} - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: file: ./coverage.xml {%- endif %} + + + minimum: + runs-on: {{ '${{ matrix.os }}' }} + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v1 + - name: Set up Python {{ '${{ matrix.python-version }}' }} + uses: actions/setup-python@v2 + with: + python-version: {{ '${{ matrix.python-version }}' }} + - name: Install package and dependencies + run: pip install invoke .[test] + - name: invoke minimum + run: invoke minimum + + + tutorials: + runs-on: {{ '${{ matrix.os }}' }} + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v1 + - name: Set up Python {{ '${{ matrix.python-version }}' }} + uses: actions/setup-python@v2 + with: + python-version: {{ '${{ matrix.python-version }}' }} + - name: Install package and dependencies + run: pip install invoke jupyter matplotlib . + - name: invoke tutorials + run: invoke tutorials diff --git a/{{cookiecutter.repository_name}}/.travis.yml b/{{cookiecutter.repository_name}}/.travis.yml index 2a9287a..474ff64 100644 --- a/{{cookiecutter.repository_name}}/.travis.yml +++ b/{{cookiecutter.repository_name}}/.travis.yml @@ -3,16 +3,14 @@ dist: trusty language: python python: - 3.8 - - 3.7 - - 3.6 - - 3.5 -{%- if cookiecutter.support_py2 == 'y' %} - - 2.7 -{%- endif %} + - 3.9 + - 3.10 + - 3.11 + - 3.12 matrix: include: - - python: 3.7 + - python: 3.8 dist: xenial sudo: required @@ -46,7 +44,7 @@ deploy: on: tags: true repo: {{ cookiecutter.github_owner }}/{{ cookiecutter.repository_name }} - python: 3.7 + python: 3.8 {%- endif %} {% if cookiecutter.use_ghpages_with_ci == 'y' %} # Automatically build and deploy documentation to GitHub Pages after every @@ -62,5 +60,5 @@ deploy: target-branch: gh-pages on: branch: master - python: 3.7 + python: 3.8 {%- endif %} diff --git a/{{cookiecutter.repository_name}}/MANIFEST.in b/{{cookiecutter.repository_name}}/MANIFEST.in deleted file mode 100644 index 9c2f658..0000000 --- a/{{cookiecutter.repository_name}}/MANIFEST.in +++ /dev/null @@ -1,13 +0,0 @@ -{% if cookiecutter.create_author_file == 'y' -%} -include AUTHORS.rst -{% endif -%} -include CONTRIBUTING.rst -include HISTORY.md -include LICENSE -include README.md - -recursive-include tests * -recursive-exclude * __pycache__ -recursive-exclude * *.py[co] - -recursive-include docs *.md *.rst conf.py Makefile make.bat *.jpg *.png *.gif diff --git a/{{cookiecutter.repository_name}}/Makefile b/{{cookiecutter.repository_name}}/Makefile index 2cd2faa..23f1f55 100644 --- a/{{cookiecutter.repository_name}}/Makefile +++ b/{{cookiecutter.repository_name}}/Makefile @@ -29,6 +29,9 @@ BROWSER := python -c "$$BROWSER_PYSCRIPT" help: @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +# INSTALL TARGETS + .PHONY: install install: clean-build clean-pyc ## install the package to the active Python's site-packages pip install . @@ -37,28 +40,69 @@ install: clean-build clean-pyc ## install the package to the active Python's sit install-test: clean-build clean-pyc ## install the package and test dependencies pip install .[test] -.PHONY: test -test: ## run tests quickly with the default Python - python -m pytest --basetemp=${ENVTMPDIR} --cov={{ cookiecutter.project_slug }} --cov-report xml +.PHONY: install-develop +install-develop: clean-build clean-pyc ## install the package in editable mode and dependencies for development + pip install -e .[dev] + +MINIMUM := $(shell sed -n '/dependencies = \[/,/]/p' pyproject.toml | grep -v -e '[][]' | sed 's/ *\(.*\),$?$$/\1/g' | tr '>' '=') + +.PHONY: install-minimum +install-minimum: ## install the minimum supported versions of the package dependencies + pip install $(MINIMUM) + +.PHONY: check-dependencies +check-dependencies: ## test if there are any broken dependencies + pip check + + +# LINT TARGETS .PHONY: lint lint: ## check style with flake8 and isort - flake8 {{ cookiecutter.project_slug }} tests - isort -c --recursive {{ cookiecutter.project_slug }} tests + invoke lint -.PHONY: install-develop -install-develop: clean-build clean-pyc ## install the package in editable mode and dependencies for development - pip install -e .[dev] +.PHONY: lint-{{ cookiecutter.project_slug }} +lint: ## check style with flake8 and isort + ruff check {{ cookiecutter.project_slug }}/ + ruff format --check --diff {{ cookiecutter.project_slug }}/ -.PHONY: test-all -test-all: ## run tests on every Python version with tox - tox -r -p auto +.PHONY: lint-tests +lint: ## check style with flake8 and isort + ruff check tests/ + ruff format --check --diff tests/ .PHONY: fix-lint fix-lint: ## fix lint issues using autoflake, autopep8, and isort - find {{ cookiecutter.project_slug }} tests -name '*.py' | xargs autoflake --in-place --remove-all-unused-imports --remove-unused-variables - autopep8 --in-place --recursive --aggressive {{ cookiecutter.project_slug }} tests - isort --apply --atomic --recursive {{ cookiecutter.project_slug }} tests + invoke fix-lint + + +# TEST TARGETS + +.PHONY: test-unit +test-unit: ## run tests quickly with the default Python + invoke unit + +.PHONY: test-readme +test-readme: ## run the readme snippets + invoke readme + +.PHONY: test-tutorials +test-tutorials: ## run the tutorial notebooks + invoke tutorials + +.PHONY: test +test: test-unit test-readme test-tutorials ## test everything that needs test dependencies + +.PHONY: test-minimum +test-minimum: install-minimum check-dependencies test ## run tests using the minimum supported dependencies + +.PHONY: test-{{ cookiecutter.project_slug }} +test: ## run tests quickly with the default Python + python -m pytest --basetemp=${ENVTMPDIR} --cov={{ cookiecutter.project_slug }} --cov-report xml + +.PHONY: test-all +test-all: ## run tests on every Python version with tox + tox -r -p auto .PHONY: coverage coverage: ## check code coverage quickly with the default Python @@ -67,6 +111,9 @@ coverage: ## check code coverage quickly with the default Python coverage html $(BROWSER) htmlcov/index.html + +# DOCS TARGETS + .PHONY: docs docs: clean-docs ## generate Sphinx HTML documentation, including API docs sphinx-apidoc --separate --no-toc -o docs/api/ {{ cookiecutter.project_slug }} @@ -80,49 +127,77 @@ view-docs: docs ## view docs in browser serve-docs: view-docs ## compile the docs watching for changes watchmedo shell-command -W -R -D -p '*.rst;*.md' -c '$(MAKE) -C docs html' . + +# RELEASE TARGETS + .PHONY: dist dist: clean ## builds source and wheel package - python setup.py sdist - python setup.py bdist_wheel + python -m build --wheel --sdist ls -l dist +.PHONY: publish-confirm +publish-confirm: + @echo "WARNING: This will irreversibly upload a new version to PyPI!" + @echo -n "Please type 'confirm' to proceed: " \ + && read answer \ + && [ "$${answer}" = "confirm" ] + .PHONY: test-publish test-publish: dist ## package and upload a release on TestPyPI twine upload --repository-url https://test.pypi.org/legacy/ dist/* .PHONY: publish -publish: dist ## package and upload a release +publish: dist publish-confirm ## package and upload a release twine upload dist/* .PHONY: bumpversion-release bumpversion-release: ## Merge master to stable and bumpversion release - git checkout stable + git checkout stable || git checkout -b stable git merge --no-ff master -m"make release-tag: Merge branch 'master' into stable" - bumpversion release + bump-my-version bump release git push --tags origin stable +.PHONY: bumpversion-release-test +bumpversion-release-test: ## Merge master to stable and bumpversion release + git checkout stable || git checkout -b stable + git merge --no-ff master -m"make release-tag: Merge branch 'master' into stable" + bump-my-version bump release --no-tag + @echo git push --tags origin stable + .PHONY: bumpversion-patch bumpversion-patch: ## Merge stable to master and bumpversion patch git checkout master git merge stable - bumpversion --no-tag patch + bump-my-version bump --no-tag patch git push .PHONY: bumpversion-minor bumpversion-minor: ## Bump the version the next minor skipping the release - bumpversion --no-tag minor + bump-my-version bump --no-tag minor .PHONY: bumpversion-major bumpversion-major: ## Bump the version the next major skipping the release - bumpversion --no-tag major + bump-my-version bump --no-tag major .PHONY: bumpversion-candidate bumpversion-candidate: ## Bump the version to the next candidate - bumpversion candidate --no-tag + bump-my-version bump candidate --no-tag + +.PHONY: bumpversion-revert +bumpversion-revert: ## Undo a previous bumpversion-release + git checkout master + git branch -D stable +CLEAN_DIR := $(shell git status --short | grep -v ??) CURRENT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) CHANGELOG_LINES := $(shell git diff HEAD..origin/stable HISTORY.md 2>&1 | wc -l) +.PHONY: check-clean +check-clean: ## Check if the directory has uncommitted changes +ifneq ($(CLEAN_DIR),) + $(error There are uncommitted changes) +endif + .PHONY: check-master check-master: ## Check if we are in master branch ifneq ($(CURRENT_BRANCH),master) @@ -137,19 +212,29 @@ endif .PHONY: check-release check-release: check-master check-history ## Check if the release can be made + @echo "A new release can be made" .PHONY: release release: check-release bumpversion-release publish bumpversion-patch +.PHONY: release-test +release-test: check-release bumpversion-release-test publish-test bumpversion-revert + .PHONY: release-candidate release-candidate: check-master publish bumpversion-candidate +.PHONY: release-candidate-test +release-candidate-test: check-clean check-master publish-test + .PHONY: release-minor release-minor: check-release bumpversion-minor release .PHONY: release-major release-major: check-release bumpversion-major release + +# CLEAN TARGETS + .PHONY: clean clean: clean-build clean-pyc clean-test clean-coverage clean-docs ## remove all build, test, coverage, docs and Python artifacts diff --git a/{{cookiecutter.repository_name}}/README.md b/{{cookiecutter.repository_name}}/README.md index fe9ecf1..e6803db 100644 --- a/{{cookiecutter.repository_name}}/README.md +++ b/{{cookiecutter.repository_name}}/README.md @@ -36,13 +36,13 @@ TODO: Provide a short overview of the project here. ## Requirements -**{{ cookiecutter.project_name }}** has been developed and tested on [Python {% if cookiecutter.support_py2 == 'y' %}2.7, {% endif %}3.5, 3.6, 3.7 and 3.8](https://www.python.org/downloads/) +**{{ cookiecutter.project_name }}** has been developed and tested on [Python 3.8, 3.9, 3.10, 3.11 and 3.12](https://www.python.org/downloads/) Also, although it is not strictly required, the usage of a [virtualenv](https://virtualenv.pypa.io/en/latest/) is highly recommended in order to avoid interfering with other software installed in the system in which **{{ cookiecutter.project_name }}** is run. -These are the minimum commands needed to create a virtualenv using python3.6 for **{{ cookiecutter.project_name }}**: +These are the minimum commands needed to create a virtualenv using python3.8 for **{{ cookiecutter.project_name }}**: ```bash pip install virtualenv diff --git a/{{cookiecutter.repository_name}}/pyproject.toml b/{{cookiecutter.repository_name}}/pyproject.toml new file mode 100644 index 0000000..dd9f04e --- /dev/null +++ b/{{cookiecutter.repository_name}}/pyproject.toml @@ -0,0 +1,194 @@ +[project] +name = '{{ cookiecutter.project_slug }}' +description = '{{ cookiecutter.project_short_description.replace("\'", "\\\'") }}' +authors = [ + {name = '{{ cookiecutter.license_owner }}', email = '{{ cookiecutter.license_owner_email }}'} +] +{%- set license_classifiers = { + 'MIT license': 'License :: OSI Approved :: MIT License', + 'BSD license': 'License :: OSI Approved :: BSD License', + 'ISC license': 'License :: OSI Approved :: ISC License (ISCL)', + 'Apache Software License 2.0': 'License :: OSI Approved :: Apache Software License', + 'GNU General Public License v3': 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)' +} %} +classifiers = [ + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Developers', +{%- if cookiecutter.open_source_license in license_classifiers %} + '{{ license_classifiers[cookiecutter.open_source_license] }}', +{%- endif %} + 'Natural Language :: English', + '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', +] +keywords = ['{{ cookiecutter.project_slug }}','{{ cookiecutter.package_name }}','{{ cookiecutter.project_name }}'] +dynamic = ['version'] +license = {text = '{{ cookiecutter.open_source_license }}'} +requires-python = '>=3.8,<3.13' +readme = 'README.md' +dependencies = [ + {%- if cookiecutter.command_line_interface|lower == 'click' %}'Click>=6.0',{%- endif %} +] + +[project.optional-dependencies] +test = [ + 'pytest>=3.4.2', + 'pytest-cov>=2.6.0', + 'pytest-runner>=2.11.1', +] + +dev = [ + '{{ cookiecutter.project_slug }}[test]', + + # general + 'build>=1.0.0,<2', + 'bump-my-version>=0.18.3', + 'pip>=9.0.1', + 'watchdog>=0.8.3', + + # docs + 'm2r>=0.2.0,<0.3', + 'Sphinx>=1.7.1,<3', + 'sphinx_rtd_theme>=0.2.4,<0.5', + 'autodocsumm>=0.1.10', + + # style check + 'ruff>=0.4.5', + + # distribute on PyPI + 'twine>=1.10.0', + 'wheel>=0.30.0', + + # Advanced testing + 'coverage>=4.5.1', + 'tox>=2.9.1', +] + +[project.urls] +homepage = 'https://github.com/{{ cookiecutter.github_owner }}/{{ cookiecutter.repository_name }}' + +[project.entry-points] +{{ cookiecutter.project_slug }} = { main = '{{ cookiecutter.project_slug }}.cli.__main__:main' } + +{% if cookiecutter.command_line_interface == 'Yes' %} +[project.scripts] +{{ cookiecutter.project_slug }} = '{{ cookiecutter.project_slug }}.cli:app' +{% endif %} + +[build-system] +requires = ['setuptools', 'wheel'] +build-backend = 'setuptools.build_meta' + +[tool.setuptools] +include-package-data = true +license-files = ['LICENSE'] + +[tool.setuptools.packages.find] +include = ['{{ cookiecutter.project_slug }}', '{{ cookiecutter.project_slug }}.*'] +namespaces = false + +[tool.setuptools.package-data] +"*" = ["*.*"] + +[tool.setuptools.exclude-package-data] +'*' = [ + '* __pycache__', +] + +[tool.setuptools.dynamic] +version = {attr = '{{ cookiecutter.project_slug }}.__version__'} + +[tool.bumpversion] +current_version = '{{ cookiecutter.version }}' +parse = '(?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))?' +serialize = [ + '{major}.{minor}.{patch}.{release}{candidate}', + '{major}.{minor}.{patch}' +] +search = '{current_version}' +replace = '{new_version}' +regex = false +ignore_missing_version = false +tag = true +sign_tags = false +tag_name = 'v{new_version}' +tag_message = 'Bump version: {current_version} → {new_version}' +allow_dirty = false +commit = true +message = 'Bump version: {current_version} → {new_version}' +commit_args = '' + +[tool.bumpversion.parts.release] +first_value = 'dev' +optional_value = 'release' +values = [ + 'dev', + 'release' +] + +[[tool.bumpversion.files]] +filename = "{{ cookiecutter.project_slug }}/__init__.py" +search = "__version__ = '{current_version}'" +replace = "__version__ = '{new_version}'" + +[tool.pytest.ini_options] +addopts = "--ignore=pyproject.toml" + +# linting +[tool.ruff] +preview = true +line-length = 99 +indent-width = 4 +src = ['{{ cookiecutter.project_slug }}'] +exclude = [ + "docs", + ".tox", + ".git", + "__pycache__", + "*.ipynb", + ".ipynb_checkpoints", + "tasks.py" +] + +[tool.ruff.lint] +select = [ + "F", # Pyflakes + "E", # pycodestyle + "W", # pycodestyle + "D", # pydocstyle + "I001", # isort + "T201", # print statements +] +ignore = [ + # pydocstyle + "D107", # Missing docstring in __init__ + "D417", # Missing argument descriptions in the docstring + "PD901", + "PD101", +] + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +preview = true +docstring-code-format = true +docstring-code-line-length = "dynamic" + +[tool.ruff.lint.isort] +known-first-party = ['{{ cookiecutter.project_slug }}'] +lines-between-types = 0 + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401", "E402", "F403", "F405", "E501", "I001"] +"tests/**.py" = ["D"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.pycodestyle] +max-doc-length = 99 +max-line-length = 99 diff --git a/{{cookiecutter.repository_name}}/setup.cfg b/{{cookiecutter.repository_name}}/setup.cfg deleted file mode 100644 index f3bb7de..0000000 --- a/{{cookiecutter.repository_name}}/setup.cfg +++ /dev/null @@ -1,50 +0,0 @@ -[bumpversion] -current_version = {{ cookiecutter.version }} -commit = True -tag = True -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? -serialize = - {major}.{minor}.{patch}.{release}{candidate} - {major}.{minor}.{patch} - -[bumpversion:part:release] -optional_value = release -first_value = dev -values = - dev - release - -[bumpversion:part:candidate] - -[bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' - -[bumpversion:file:{{ cookiecutter.project_slug }}/__init__.py] -search = __version__ = '{current_version}' -replace = __version__ = '{new_version}' - -[bdist_wheel] -universal = 1 - -[flake8] -max-line-length = 99 -exclude = docs, .tox, .git, __pycache__, .ipynb_checkpoints -# Ignore W503 (line break before binary operator) -# Line breaks should occur before the binary operator because it keeps all operators aligned. -# Please discuss with the group before adding other ignores. -ignore = W503 - -[isort] -include_trailing_comma = True -line_length=99 -lines_between_types = 0 -multi_line_output = 4 -not_skip = __init__.py -use_parentheses = True - -[aliases] -test = pytest - -[tool:pytest] -collect_ignore = ['setup.py'] diff --git a/{{cookiecutter.repository_name}}/setup.py b/{{cookiecutter.repository_name}}/setup.py deleted file mode 100644 index b35fc60..0000000 --- a/{{cookiecutter.repository_name}}/setup.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""The setup script.""" - -from setuptools import setup, find_packages - -with open('README.md', encoding='utf-8') as readme_file: - readme = readme_file.read() - -with open('HISTORY.md', encoding='utf-8') as history_file: - history = history_file.read() - -install_requires = [ - {%- if cookiecutter.command_line_interface|lower == 'click' %}'Click>=6.0',{%- endif %} - {%- if cookiecutter.support_py2 == 'y' %}'six',{%- endif %} -] - -setup_requires = [ - 'pytest-runner>=2.11.1', -] - -tests_require = [ - 'pytest>=3.4.2', - 'pytest-cov>=2.6.0', -] - -development_requires = [ - # general - 'bumpversion>=0.5.3', - 'pip>=9.0.1', - 'watchdog>=0.8.3', - - # docs - 'm2r>=0.2.0,<0.3', - 'Sphinx>=1.7.1,<3', - 'sphinx_rtd_theme>=0.2.4,<0.5', - 'autodocsumm>=0.1.10', - - # style check - 'flake8>=3.7.7', - 'isort>=4.3.4', - - # fix style issues - 'autoflake>=1.2', - 'autopep8>=1.4.3', - - # distribute on PyPI - 'twine>=1.10.0', - 'wheel>=0.30.0', - - # Advanced testing - 'coverage>=4.5.1', - 'tox>=2.9.1', -] - -{%- set license_classifiers = { - 'MIT license': 'License :: OSI Approved :: MIT License', - 'BSD license': 'License :: OSI Approved :: BSD License', - 'ISC license': 'License :: OSI Approved :: ISC License (ISCL)', - 'Apache Software License 2.0': 'License :: OSI Approved :: Apache Software License', - 'GNU General Public License v3': 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)' -} %} - -setup( - author='{{ cookiecutter.license_owner }}', - author_email='{{ cookiecutter.license_owner_email }}', - classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Developers', -{%- if cookiecutter.open_source_license in license_classifiers %} - '{{ license_classifiers[cookiecutter.open_source_license] }}', -{%- endif %} - 'Natural Language :: English', -{%- if cookiecutter.support_py2 == 'y' %} - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', -{%- endif %} - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - ], - description='{{ cookiecutter.project_short_description.replace("\'", "\\\'") }}', - {%- if 'no' not in cookiecutter.command_line_interface|lower %} - entry_points={ - 'console_scripts': [ - '{{ cookiecutter.package_name }}={{ cookiecutter.project_slug }}.cli:main', - ], - }, - {%- endif %} - extras_require={ - 'test': tests_require, - 'dev': development_requires + tests_require, - }, - install_package_data=True, - install_requires=install_requires, -{%- if cookiecutter.open_source_license in license_classifiers %} - license='{{ cookiecutter.open_source_license }}', -{%- endif %} - long_description=readme + '\n\n' + history, - long_description_content_type='text/markdown', - include_package_data=True, - keywords='{{ cookiecutter.project_slug }} {{ cookiecutter.package_name }} {{ cookiecutter.project_name }}', - name='{{ cookiecutter.package_name }}', - packages=find_packages(include=['{{ cookiecutter.project_slug }}', '{{ cookiecutter.project_slug }}.*']), -{%- if cookiecutter.support_py2 == 'y' %} - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', -{%- else %} - python_requires='>=3.5', -{%- endif %} - setup_requires=setup_requires, - test_suite='tests', - tests_require=tests_require, - url='https://github.com/{{ cookiecutter.github_owner }}/{{ cookiecutter.repository_name }}', - version='{{ cookiecutter.version }}', - zip_safe=False, -) diff --git a/{{cookiecutter.repository_name}}/tasks.py b/{{cookiecutter.repository_name}}/tasks.py new file mode 100644 index 0000000..5bceda8 --- /dev/null +++ b/{{cookiecutter.repository_name}}/tasks.py @@ -0,0 +1,134 @@ +import glob +import operator +import os +import pkg_resources +import platform +import re +import shutil +import stat +from pathlib import Path + +from invoke import task + + +COMPARISONS = { + '>=': operator.ge, + '>': operator.gt, + '<': operator.lt, + '<=': operator.le +} + + +@task +def check_dependencies(c): + c.run('python -m pip check') + + +@task +def unit(c): + c.run('python -m pytest --cov={{cookiecutter.repository_name}} --cov-report=xml') + + +def _validate_python_version(line): + is_valid = True + for python_version_match in re.finditer(r"python_version(<=?|>=?|==)\'(\d\.?)+\'", line): + python_version = python_version_match.group(0) + comparison = re.search(r'(>=?|<=?|==)', python_version).group(0) + version_number = python_version.split(comparison)[-1].replace("'", "") + comparison_function = COMPARISONS[comparison] + is_valid = is_valid and comparison_function( + pkg_resources.parse_version(platform.python_version()), + pkg_resources.parse_version(version_number), + ) + + return is_valid + + +@task +def install_minimum(c): + with open('pyproject.toml', 'r') as pyproject_file: + lines = pyproject_file.read().splitlines() + + versions = [] + started = False + for line in lines: + if started: + if line == ']': + started = False + continue + + line = line.strip() + if _validate_python_version(line): + requirement = re.match(r'[^>]*', line).group(0) + requirement = re.sub(r"""['",]""", '', requirement) + version = re.search(r'>=?(\d\.?)+\w*', line).group(0) + if version: + version = re.sub(r'>=?', '==', version) + version = re.sub(r"""['",]""", '', version) + requirement += version + versions.append(requirement) + + elif (line.startswith('dependencies = [')): + started = True + + c.run(f'python -m pip install {" ".join(versions)}') + + +@task +def minimum(c): + install_minimum(c) + check_dependencies(c) + unit(c) + + +@task +def readme(c): + test_path = Path('tests/readme_test') + if test_path.exists() and test_path.is_dir(): + shutil.rmtree(test_path) + + cwd = os.getcwd() + os.makedirs(test_path, exist_ok=True) + shutil.copy('README.md', test_path / 'README.md') + os.chdir(test_path) + c.run('rundoc run --single-session python3 -t python3 README.md') + os.chdir(cwd) + shutil.rmtree(test_path) + + +@task +def tutorials(c): + for ipynb_file in glob.glob('tutorials/*.ipynb'): + if '.ipynb_checkpoints' not in ipynb_file: + c.run(( + 'jupyter nbconvert --execute --ExecutePreprocessor.timeout=3600 ' + f'--to=html --stdout {ipynb_file}' + ), hide='out') + + +@task +def lint(c): + check_dependencies(c) + c.run('ruff check .') + c.run('ruff format --check --diff .') + + +@task +def fix_lint(c): + check_dependencies(c) + c.run('ruff check --fix .') + c.run('ruff format .') + + +def remove_readonly(func, path, _): + "Clear the readonly bit and reattempt the removal" + os.chmod(path, stat.S_IWRITE) + func(path) + + +@task +def rmdir(c, path): + try: + shutil.rmtree(path, onerror=remove_readonly) + except PermissionError: + pass \ No newline at end of file diff --git a/{{cookiecutter.repository_name}}/tox.ini b/{{cookiecutter.repository_name}}/tox.ini index 53accf0..54846f9 100644 --- a/{{cookiecutter.repository_name}}/tox.ini +++ b/{{cookiecutter.repository_name}}/tox.ini @@ -1,29 +1,25 @@ [tox] -envlist = {% if cookiecutter.support_py2 == 'y' %}py27, {% endif %}py35, py36, py37, py38, lint, docs +envlist = py38, py39, py310, py311, py312, lint, docs {%- if cookiecutter.ci_provider == 'Travis CI' %} [travis] python = 3.8: py38 - 3.7: py37, docs, lint - 3.6: py36 - 3.5: py35 -{%- if cookiecutter.support_py2 == 'y' %} - 2.7: py27 -{%- endif %} + 3.9: py39, docs, lint + 3.10: py310 + 3.11: py311 + 3.12: py312 {%- endif %} {%- if cookiecutter.ci_provider == 'Github Actions' %} [gh-actions] python = 3.8: py38 - 3.7: py37, docs, lint - 3.6: py36 - 3.5: py35 -{%- if cookiecutter.support_py2 == 'y' %} - 2.7: py27 -{%- endif %} + 3.9: py39, docs, lint + 3.10: py310 + 3.11: py311 + 3.12: py312 {%- endif %} [testenv] @@ -34,16 +30,14 @@ extras = test commands = /usr/bin/env make test - [testenv:lint] skipsdist = true extras = dev commands = /usr/bin/env make lint - [testenv:docs] skipsdist = true extras = dev commands = - /usr/bin/env make docs + /usr/bin/env make docs \ No newline at end of file