From 10e3cf25c96a5365023a28d4e2960e24b9837157 Mon Sep 17 00:00:00 2001 From: Chuck Daniels Date: Wed, 7 Aug 2024 22:55:11 -0400 Subject: [PATCH] Project clean up and maintenance (#11) --- .github/dependabot.yml | 7 + .github/workflows/ci.yml | 71 ++++---- .gitignore | 282 ++++++++++++++++++++++++++++- Makefile | 22 +-- README.md | 31 ++-- pyproject.toml | 19 +- requirements-dev.txt | 7 +- setup.cfg | 2 + src/maybe/maybe.py | 4 +- tests/type-checking/test_maybe.yml | 18 ++ tox.ini | 12 +- 11 files changed, 381 insertions(+), 94 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..44a3d04 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4d4054..6e5968e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,74 +8,65 @@ on: name: CI jobs: - test: runs-on: ubuntu-latest strategy: matrix: python: - - '3.11' - - '3.10' - - '3.9' - - '3.8' - name: Python ${{ matrix.python }} + - version: '3.12' + - version: '3.11' + - version: '3.10' + - version: '3.9' + exclude-pattern-matching: true + - version: '3.8' + exclude-pattern-matching: true + name: Python ${{ matrix.python.version }} steps: - # Python - - name: Setup python ${{ matrix.python }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - # Check out code - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - # Cached dependencies - - uses: actions/cache@v1 + # Python + - name: Setup python ${{ matrix.python.version }} + uses: actions/setup-python@v5 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-py${{ matrix.python }}-${{ hashFiles('**/requirements-dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip-py${{ matrix.python }}- + python-version: ${{ matrix.python.version }} + cache: pip + cache-dependency-path: requirements-dev.txt + - name: Install dev dependencies - run: pip install --requirement requirements-dev.txt + run: pip install --root-user-action=ignore --requirement requirements-dev.txt # Install library - name: Install maybe - run: pip install --editable . + run: pip install --root-user-action=ignore --editable . # Tests - - name: Run tests - run: pytest --ignore=tests/test_pattern_matching.py --ignore=tests/type-checking/test_maybe.yml - - name: Run tests (type checking) - if: matrix.python != '3.8' && matrix.python != '3.9' - # These started breaking for <= 3.9, due to the type checker using a - # '|' for unions rather than 'Union[...]', so it's not possible to run - # the tests without maintaining two duplicate files (one for <= 3.9 and - # one for > 3.9) - run: pytest tests/type-checking/test_maybe.yml - - name: Run tests (pattern matching) - if: matrix.python == '3.10' || matrix.python == '3.11' - run: pytest tests/test_pattern_matching.py + - name: Run tests (excluding pattern matching) + if: ${{ matrix.python.exclude-pattern-matching }} + run: pytest --ignore=tests/test_pattern_matching.py + - name: Run tests (including pattern matching) + if: ${{ ! matrix.python.exclude-pattern-matching }} + run: pytest # Linters - - name: Run flake8 (Python >= 3.10) - run: flake8 - if: matrix.python != '3.9' && matrix.python != '3.8' - - name: Run flake8 (Python < 3.10) + - name: Run flake8 (excluding pattern matching) + if: ${{ matrix.python.exclude-pattern-matching }} run: flake8 --extend-exclude tests/test_pattern_matching.py - if: matrix.python == '3.9' || matrix.python == '3.8' + - name: Run flake8 (including pattern matching) + if: ${{ ! matrix.python.exclude-pattern-matching }} + run: flake8 - name: Run mypy run: mypy # Packaging - name: Build packages run: | - pip install --upgrade build pip setuptools wheel + pip install --root-user-action=ignore --upgrade build pip setuptools wheel python -m build # Coverage - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 if: matrix.python == '3.9' with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index c3302ed..73a5f5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,275 @@ -.cache/ -.coverage -coverage.xml -*.swp -*.pyc -__pycache__ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ *.egg-info/ -build/ -.idea/ -.mypy_cache/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# IPython +profile_default/ +ipython_config.py + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Environments +.env +.venv +env/ venv/ -/.tox/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +### Python Patch ### + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python +# Created by https://www.toptal.com/developers/gitignore/api/vim +# Edit at https://www.toptal.com/developers/gitignore?templates=vim + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# End of https://www.toptal.com/developers/gitignore/api/vim +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode + +### VisualStudioCode ### +.vscode/ +# !.vscode/settings.json +# !.vscode/tasks.json +# !.vscode/launch.json +# !.vscode/extensions.json +# !.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode +# Created by https://www.toptal.com/developers/gitignore/api/jetbrains +# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains + +### JetBrains ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +# End of https://www.toptal.com/developers/gitignore/api/jetbrains diff --git a/Makefile b/Makefile index dbd4c58..203790e 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,23 @@ # phony trick from https://keleshev.com/my-book-writing-setup/ .PHONY: phony +# "True" if running Python < (3, 10); "False" otherwise. +PYTHON_PRE_310 := $(shell python -c "import sys; print(sys.version_info < (3, 10))") + install: phony -ifndef VIRTUAL_ENV - $(error install can only be run inside a Python virtual environment) -endif @echo Installing dependencies... - pip install -r requirements-dev.txt - pip install -e . + pip install --require-virtualenv -r requirements-dev.txt + pip install --require-virtualenv -e . lint: phony lint-flake lint-mypy -lint-flake: phony - flake8 - -lint-flake-pre310: phony - # Python <3.10 doesn't support pattern matching. +lint-flake: +ifeq ($(PYTHON_PRE_310), True) + @# Python <3.10 doesn't support pattern matching. flake8 --extend-exclude tests/test_pattern_matching.py +else + flake8 +endif lint-mypy: phony mypy @@ -29,4 +30,3 @@ docs: phony --overview-file README.md \ --src-base-url https://github.com/rustedpy/maybe/blob/main/ \ ./src/maybe - diff --git a/README.md b/README.md index 5d2ddc8..083711d 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,13 @@ https://doc.rust-lang.org/std/option/), fully type annotated. Latest release: ```sh -$ pip install rustedpy-maybe +pip install rustedpy-maybe ``` - Latest GitHub `master` branch version: ```sh -$ pip install git+https://github.com/rustedpy/maybe +pip install git+https://github.com/rustedpy/maybe ``` ## Summary @@ -47,19 +46,19 @@ and `make` installed. On Windows, you will need to refer to the Python documentation (linked below) and reference the `Makefile` for commands to run from the non-unix shell you're using on Windows. -1. Setup and activate a virtual environment. See [Python docs][pydocs-venv] for more - information about virtual environments and setup. -2. Run `make install` to install dependencies -3. Switch to a new git branch and make your changes -4. Test your changes: - - `make test` - - `make lint` - - You can also start a Python REPL and import `maybe` -5. Update documentation - - Edit any relevant docstrings, markdown files - - Run `make docs` -6. Add an entry to the [changelog](./CHANGELOG.md) -5. Git commit all your changes and create a new PR. +1. Setup and activate a virtual environment. See [Python docs][pydocs-venv] for + more information about virtual environments and setup. +1. Run `make install` to install dependencies +1. Switch to a new git branch and make your changes +1. Test your changes: + - `make test` + - `make lint` + - You can also start a Python REPL and import `maybe` +1. Update documentation + - Edit any relevant docstrings, markdown files + - Run `make docs` +1. Add an entry to the [changelog](./CHANGELOG.md) +1. Git commit all your changes and create a new PR. [pydocs-venv]: https://docs.python.org/3/library/venv.html diff --git a/pyproject.toml b/pyproject.toml index 10d29dd..cc8edb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,7 @@ build-backend = "setuptools.build_meta" [tool.mypy] python_version = "3.11" -files = [ - "src", - "tests", -] +files = ["src", "tests"] # Exclude files with pattern matching syntax until we drop support for Python # versions that don't support pattern matching. Trying to use with an older # Python version results in a "invalid syntax" error from mypy @@ -32,17 +29,19 @@ warn_return_any = true warn_unused_configs = true warn_unused_ignores = true +[tool.coverage.run] +# Ignore "Couldn't parse Python file" warnings produced when attempting to parse +# Python 3.10+ code using an earlier version of Python. +disable_warnings = ["couldnt-parse"] + [tool.pytest.ini_options] addopts = [ "--tb=short", - "--cov=maybe", - "--cov=tests", - "--cov-report=term", + "--cov=src", + "--cov-report=term-missing", "--cov-report=xml", # By default, ignore tests that only run on Python 3.10+ "--ignore=tests/test_pattern_matching.py", ] -testpaths = [ - "tests", -] +testpaths = ["tests"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 259b0fb..082e9ba 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,9 @@ build flake8 -twine +lazydocs +mypy pytest +pytest-asyncio pytest-cov -mypy pytest-mypy-plugins -pytest-asyncio +twine diff --git a/setup.cfg b/setup.cfg index 0c8bb7f..0ea44bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: 3 :: Only [options] @@ -43,4 +44,5 @@ max-line-length = 99 exclude = .direnv/ .tox/ + .venv/ venv/ diff --git a/src/maybe/maybe.py b/src/maybe/maybe.py index 679a657..b10a68f 100644 --- a/src/maybe/maybe.py +++ b/src/maybe/maybe.py @@ -281,7 +281,7 @@ def maybe(self) -> Maybe[Any]: def is_some(maybe: Maybe[T]) -> TypeGuard[Some[T]]: - """A typeguard to check if a maybe is an Some + """A typeguard to check if a maybe is a Some Usage: >>> r: Maybe[int, str] = get_a_maybe() @@ -294,7 +294,7 @@ def is_some(maybe: Maybe[T]) -> TypeGuard[Some[T]]: def is_nothing(maybe: Maybe[T]) -> TypeGuard[Nothing]: - """A typeguard to check if a maybe is an Nothing + """A typeguard to check if a maybe is a Nothing Usage: >>> r: Maybe[int, str] = get_a_maybe() diff --git a/tests/type-checking/test_maybe.yml b/tests/type-checking/test_maybe.yml index 505440f..2861e02 100644 --- a/tests/type-checking/test_maybe.yml +++ b/tests/type-checking/test_maybe.yml @@ -35,9 +35,27 @@ res6 = res4.or_else(toint) reveal_type(res6) # N: Revealed type is "maybe.maybe.Some[builtins.int]" +- case: covariance_pre310 + skip: "sys.version_info >= (3, 10)" + disable_cache: false + main: | + from maybe import Maybe, Some, Nothing + + some_int: Some[int] = Some(42) + some_float: Some[float] = some_int + some_int = some_float # E: Incompatible types in assignment (expression has type "Some[float]", variable has type "Some[int]") [assignment] + + nothing: Nothing = Nothing() + + maybe_int: Maybe[int] = some_int or nothing + maybe_float: Maybe[float] = maybe_int + maybe_int = maybe_float # E: Incompatible types in assignment (expression has type "Union[Some[float], Nothing]", variable has type "Union[Some[int], Nothing]") [assignment] + - case: covariance + skip: "sys.version_info < (3, 10)" disable_cache: false main: | + import sys from maybe import Maybe, Some, Nothing some_int: Some[int] = Some(42) diff --git a/tox.ini b/tox.ini index a2063f8..6257c4e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,12 @@ [tox] -envlist = py311,py310,py39,py38 +; Version 4 rewrite fixed https://github.com/tox-dev/tox/issues/1297, which was +; causing `usedevelop = true` to be ignored. +min_version = 4.0 +envlist = py312,py311,py310,py39,py38 [testenv] +; Required for test coverage to work correctly +usedevelop = true deps = -rrequirements-dev.txt commands = pytest {posargs} @@ -9,4 +14,7 @@ commands = pytest {posargs} deps = -rrequirements-dev.txt commands = pytest {posargs} - pytest {posargs} tests/test_pattern_matching.py + ; Reset coverage options since we don't need to report coverage + ; for testing pattern matching, which erroneously shows misses for + ; code covered by the preceding command. + pytest {posargs} --cov-reset tests/test_pattern_matching.py