diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..21c125c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +.py text eol=lf +.rst text eol=lf +.txt text eol=lf +.yaml text eol=lf +.toml text eol=lf +.license text eol=lf +.md text eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22f6582..041a337 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,68 +10,5 @@ jobs: test: runs-on: ubuntu-latest steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - name: Translate Repo Name For Build Tools filename_prefix - id: repo-name - run: | - echo ::set-output name=repo-name::$( - echo ${{ github.repository }} | - awk -F '\/' '{ print tolower($2) }' | - tr '_' '-' - ) - - name: Set up Python 3.x - uses: actions/setup-python@v2 - with: - python-version: "3.x" - - name: Versions - run: | - python3 --version - - name: Checkout Current Repo - uses: actions/checkout@v1 - with: - submodules: true - - name: Checkout tools repo - uses: actions/checkout@v2 - with: - repository: adafruit/actions-ci-circuitpython-libs - path: actions-ci - - name: Install dependencies - # (e.g. - apt-get: gettext, etc; pip: circuitpython-build-tools, requirements.txt; etc.) - run: | - source actions-ci/install.sh - - name: Pip install Sphinx, pre-commit - run: | - pip install --force-reinstall Sphinx sphinx-rtd-theme pre-commit - - name: Library version - run: git describe --dirty --always --tags - - name: Setup problem matchers - uses: adafruit/circuitpython-action-library-ci-problem-matchers@v1 - - name: Pre-commit hooks - run: | - pre-commit run --all-files - - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . - - name: Archive bundles - uses: actions/upload-artifact@v2 - with: - name: bundles - path: ${{ github.workspace }}/bundles/ - - name: Build docs - working-directory: docs - run: sphinx-build -E -W -b html . _build/html - - name: Check For pyproject.toml - id: need-pypi - run: | - echo ::set-output name=pyproject-toml::$( find . -wholename './pyproject.toml' ) - - name: Build Python package - if: contains(steps.need-pypi.outputs.pyproject-toml, 'pyproject.toml') - run: | - pip install --upgrade build twine - for file in $(find -not -path "./.*" -not -path "./docs*" \( -name "*.py" -o -name "*.toml" \) ); do - sed -i -e "s/0.0.0-auto.0/1.2.3/" $file; - done; - python -m build - twine check dist/* + - name: Run Build CI workflow + uses: adafruit/workflows-circuitpython-libs/build@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index d1b4f8d..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,88 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -name: Release Actions - -on: - release: - types: [published] - -jobs: - upload-release-assets: - runs-on: ubuntu-latest - steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - name: Translate Repo Name For Build Tools filename_prefix - id: repo-name - run: | - echo ::set-output name=repo-name::$( - echo ${{ github.repository }} | - awk -F '\/' '{ print tolower($2) }' | - tr '_' '-' - ) - - name: Set up Python 3.x - uses: actions/setup-python@v2 - with: - python-version: "3.x" - - name: Versions - run: | - python3 --version - - name: Checkout Current Repo - uses: actions/checkout@v1 - with: - submodules: true - - name: Checkout tools repo - uses: actions/checkout@v2 - with: - repository: adafruit/actions-ci-circuitpython-libs - path: actions-ci - - name: Install deps - run: | - source actions-ci/install.sh - - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . - - name: Upload Release Assets - # the 'official' actions version does not yet support dynamically - # supplying asset names to upload. @csexton's version chosen based on - # discussion in the issue below, as its the simplest to implement and - # allows for selecting files with a pattern. - # https://github.com/actions/upload-release-asset/issues/4 - #uses: actions/upload-release-asset@v1.0.1 - uses: csexton/release-asset-action@master - with: - pattern: "bundles/*" - github-token: ${{ secrets.GITHUB_TOKEN }} - - upload-pypi: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Check For pyproject.toml - id: need-pypi - run: | - echo ::set-output name=pyproject-toml::$( find . -wholename './pyproject.toml' ) - - name: Set up Python - if: contains(steps.need-pypi.outputs.pyproject-toml, 'pyproject.toml') - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - if: contains(steps.need-pypi.outputs.pyproject-toml, 'pyproject.toml') - run: | - python -m pip install --upgrade pip - pip install --upgrade build twine - - name: Build and publish - if: contains(steps.need-pypi.outputs.pyproject-toml, 'pyproject.toml') - env: - TWINE_USERNAME: ${{ secrets.pypi_username }} - TWINE_PASSWORD: ${{ secrets.pypi_password }} - run: | - for file in $(find -not -path "./.*" -not -path "./docs*" \( -name "*.py" -o -name "*.toml" \) ); do - sed -i -e "s/0.0.0-auto.0/${{github.event.release.tag_name}}/" $file; - done; - python -m build - twine upload dist/* diff --git a/.github/workflows/release_gh.yml b/.github/workflows/release_gh.yml new file mode 100644 index 0000000..9acec60 --- /dev/null +++ b/.github/workflows/release_gh.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: GitHub Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + runs-on: ubuntu-latest + steps: + - name: Run GitHub Release CI workflow + uses: adafruit/workflows-circuitpython-libs/release-gh@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + upload-url: ${{ github.event.release.upload_url }} diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml new file mode 100644 index 0000000..65775b7 --- /dev/null +++ b/.github/workflows/release_pypi.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: PyPI Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + runs-on: ubuntu-latest + steps: + - name: Run PyPI Release CI workflow + uses: adafruit/workflows-circuitpython-libs/release-pypi@main + with: + pypi-username: ${{ secrets.pypi_username }} + pypi-password: ${{ secrets.pypi_password }} diff --git a/.gitignore b/.gitignore index 544ec4a..87b9508 100644 --- a/.gitignore +++ b/.gitignore @@ -32,11 +32,18 @@ __pycache__ # Sphinx build-specific files _build +# MyPy-specific type-checking files +.mypy_cache + +# pip install files +/build/ + # This file results from running `pip -e install .` in a local repository *.egg-info # Virtual environment-specific files .env +.venv # MacOS-specific files *.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3343606..ff19dde 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,42 +1,21 @@ -# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò +# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries # # SPDX-License-Identifier: Unlicense repos: - - repo: https://github.com/python/black - rev: 22.3.0 - hooks: - - id: black - - repo: https://github.com/fsfe/reuse-tool - rev: v0.14.0 - hooks: - - id: reuse - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.5.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/pycqa/pylint - rev: v2.11.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.4 hooks: - - id: pylint - name: pylint (library code) - types: [python] - args: - - --disable=consider-using-f-string - exclude: "^(docs/|examples/|tests/|setup.py$)" - - id: pylint - name: pylint (example code) - description: Run pylint rules on "examples/*.py" files - types: [python] - files: "^examples/" - args: - - --disable=missing-docstring,invalid-name,consider-using-f-string,duplicate-code - - id: pylint - name: pylint (test code) - description: Run pylint rules on "tests/*.py" files - types: [python] - files: "^tests/" - args: - - --disable=missing-docstring,consider-using-f-string,duplicate-code + - id: ruff-format + - id: ruff + args: ["--fix"] + - repo: https://github.com/fsfe/reuse-tool + rev: v3.0.1 + hooks: + - id: reuse diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index f772971..0000000 --- a/.pylintrc +++ /dev/null @@ -1,436 +0,0 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: Unlicense - -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Add files or directories to the ignore-list. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the ignore-list. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -# disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call -disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation,unspecified-encoding - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable= - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio).You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -# notes=FIXME,XXX,TODO -notes=FIXME,XXX - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules=board - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,future.builtins - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -# expected-line-ending-format= -expected-line-ending-format=LF - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=yes - -# Minimum lines number of a similarity. -min-similarity-lines=12 - - -[BASIC] - -# Naming hint for argument names -argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Regular expression matching correct argument names -argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Naming hint for attribute names -attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Regular expression matching correct attribute names -attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class names -# class-name-hint=[A-Z_][a-zA-Z0-9]+$ -class-name-hint=[A-Z_][a-zA-Z0-9_]+$ - -# Regular expression matching correct class names -# class-rgx=[A-Z_][a-zA-Z0-9]+$ -class-rgx=[A-Z_][a-zA-Z0-9_]+$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming hint for function names -function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Regular expression matching correct function names -function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Good variable names which should always be accepted, separated by a comma -# good-names=i,j,k,ex,Run,_ -good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for method names -method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Regular expression matching correct method names -method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Naming hint for variable names -variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - -# Regular expression matching correct variable names -variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Maximum number of attributes for a class (see R0902). -# max-attributes=7 -max-attributes=11 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of statements in function / method body -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=1 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 33c2a61..88bca9f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,6 +8,9 @@ # Required version: 2 +sphinx: + configuration: docs/conf.py + build: os: ubuntu-20.04 tools: diff --git a/README.rst b/README.rst index 3ff3f72..b61954f 100644 --- a/README.rst +++ b/README.rst @@ -13,9 +13,9 @@ Introduction :target: https://github.com/adafruit/Adafruit_CircuitPython_turtle/actions/ :alt: Build Status -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code Style: Black +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Code Style: Ruff Turtle graphics library for CircuitPython and displayio diff --git a/adafruit_turtle.py b/adafruit_turtle.py index 5944da4..4022ed9 100644 --- a/adafruit_turtle.py +++ b/adafruit_turtle.py @@ -24,16 +24,24 @@ https://github.com/adafruit/Adafruit_CircuitPython_BusDevice """ +from __future__ import annotations + # pylint:disable=too-many-public-methods, too-many-instance-attributes, invalid-name # pylint:disable=too-few-public-methods, too-many-lines, too-many-arguments - import gc import math import time -import board + import displayio -__version__ = "0.0.0-auto.0" +try: + from typing import List, Optional, Tuple, Union + + import busdisplay +except ImportError: + pass + +__version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_turtle.git" @@ -41,21 +49,84 @@ class Color: """Standard colors""" WHITE = 0xFFFFFF + """0xFFFFFF + + :meta hide-value:""" + BLACK = 0x000000 + """0x000000 + + :meta hide-value:""" + RED = 0xFF0000 + """0xFF0000 + + :meta hide-value:""" + ORANGE = 0xFFA500 + """0xFFA500 + + :meta hide-value:""" + YELLOW = 0xFFEE00 + """0xFFEE00 + + :meta hide-value:""" + GREEN = 0x00C000 + """0x00C000 + + :meta hide-value:""" + BLUE = 0x0000FF + """0x0000FF + + :meta hide-value:""" + PURPLE = 0x8040C0 + """0x8040C0 + + :meta hide-value:""" + PINK = 0xFF40C0 + """0xFF40C0 + + :meta hide-value:""" + LIGHT_GRAY = 0xAAAAAA + """0xAAAAAA + + :meta hide-value:""" + GRAY = 0x444444 + """0x444444 + + :meta hide-value:""" + BROWN = 0xCA801D + """0xCA801D + + :meta hide-value:""" + DARK_GREEN = 0x008700 + """0x008700 + + :meta hide-value:""" + TURQUOISE = 0x00C0C0 + """0x00C0C0 + + :meta hide-value:""" + DARK_BLUE = 0x0000AA + """0x0000AA + + :meta hide-value:""" + DARK_RED = 0x800000 + """0x800000 + + :meta hide-value:""" colors = ( BLACK, @@ -76,11 +147,11 @@ class Color: DARK_RED, ) - def __init__(self): + def __init__(self) -> None: pass -class Vec2D(tuple): +class Vec2D: """A 2 dimensional vector class, used as a helper class for implementing turtle graphics. May be useful for turtle graphics programs also. @@ -94,32 +165,35 @@ class Vec2D(tuple): # k*a and a*k multiplication with scalar # |a| absolute value of a # a.rotate(angle) rotation - def __init__(self, x, y): - super().__init__((x, y)) + def __init__(self, x: float, y: float) -> None: + self.values = (x, y) + + def __getitem__(self, index: int) -> float: + return self.values[index] - def __add__(self, other): + def __add__(self, other: Vec2D) -> Vec2D: return Vec2D(self[0] + other[0], self[1] + other[1]) - def __mul__(self, other): + def __mul__(self, other: Union[float, Vec2D]) -> Union[float, Vec2D]: if isinstance(other, Vec2D): return self[0] * other[0] + self[1] * other[1] return Vec2D(self[0] * other, self[1] * other) - def __rmul__(self, other): + def __rmul__(self, other: float) -> Optional[Vec2D]: if isinstance(other, (float, int)): return Vec2D(self[0] * other, self[1] * other) return None - def __sub__(self, other): + def __sub__(self, other: Vec2D) -> Vec2D: return Vec2D(self[0] - other[0], self[1] - other[1]) - def __neg__(self): + def __neg__(self) -> Vec2D: return Vec2D(-self[0], -self[1]) - def __abs__(self): + def __abs__(self) -> float: return (self[0] ** 2 + self[1] ** 2) ** 0.5 - def rotate(self, angle): + def rotate(self, angle: float) -> Vec2D: """Rotate self counterclockwise by angle. :param angle: how much to rotate @@ -130,44 +204,42 @@ def rotate(self, angle): c, s = math.cos(angle), math.sin(angle) return Vec2D(self[0] * c + perp[0] * s, self[1] * c + perp[1] * s) - def __getnewargs__(self): + def __getnewargs__(self) -> Tuple[float, float]: return (self[0], self[1]) - def __repr__(self): - return "({:.2f},{:.2f})".format(self[0], self[1]) + def __repr__(self) -> str: + return f"({self[0]:.2f},{self[1]:.2f})" class turtle: """A Turtle that can be given commands to draw.""" - # pylint:disable=too-many-statements - def __init__(self, display=None, scale=1): - + def __init__(self, display: Optional[busdisplay.BusDisplay] = None, scale: float = 1) -> None: if display: self._display = display else: try: + import board + self._display = board.DISPLAY except AttributeError as err: - raise RuntimeError( - "No display available. One must be provided." - ) from err + raise RuntimeError("No display available. One must be provided.") from err - self._w = self._display.width - self._h = self._display.height + self._w: int = self._display.width + self._h: int = self._display.height self._x = self._w // (2 * scale) self._y = self._h // (2 * scale) self._speed = 6 - self._heading = 0 - self._logomode = True + self._heading: float = 0 self._fullcircle = 360.0 self._degreesPerAU = 1.0 - self._angleOrient = 1 - self._angleOffset = 0 + self._logomode = False + self._angleOrient = -1 + self._angleOffset: float = self._fullcircle / 4 self._bg_color = 0 - self._splash = displayio.Group() - self._bgscale = 1 + self._splash: displayio.Group = displayio.Group() + self._bgscale: int = 1 if self._w == self._h: i = 1 while self._bgscale == 1: @@ -191,7 +263,7 @@ def __init__(self, display=None, scale=1): # group to add background pictures (and/or user-defined stuff) self._bg_addon_group = displayio.Group() self._splash.append(self._bg_addon_group) - self._fg_scale = scale + self._fg_scale: int = int(scale) self._w = self._w // self._fg_scale self._h = self._h // self._fg_scale self._fg_bitmap = displayio.Bitmap(self._w, self._h, len(Color.colors)) @@ -236,47 +308,46 @@ def __init__(self, display=None, scale=1): self._turtle_pic = None self._turtle_odb = None self._turtle_alt_sprite = None + self._turtle_x = self._x + self._turtle_y = self._y self._drawturtle() self._stamps = {} self._turtle_odb_use = 0 self._turtle_odb_file = None self._odb_tilegrid = None gc.collect() - self._display.show(self._splash) + self._display.root_group = self._splash # pylint:enable=too-many-statements - def _drawturtle(self): + def _drawturtle(self) -> None: if self._turtle_pic is None: - self._turtle_sprite.x = int(self._x - 4) - self._turtle_sprite.y = int(self._y - 4) + self._turtle_sprite.x = int(self._turtle_x - 4) + self._turtle_sprite.y = int(self._turtle_y - 4) + elif self._turtle_odb is not None: + self._turtle_alt_sprite.x = int(self._turtle_x - self._turtle_odb.width // 2) + self._turtle_alt_sprite.y = int(self._turtle_y - self._turtle_odb.height // 2) else: - if self._turtle_odb is not None: - self._turtle_alt_sprite.x = int(self._x - self._turtle_odb.width // 2) - self._turtle_alt_sprite.y = int(self._y - self._turtle_odb.height // 2) - else: - self._turtle_alt_sprite.x = int(self._x - self._turtle_pic[0] // 2) - self._turtle_alt_sprite.y = int(self._y - self._turtle_pic[1] // 2) + self._turtle_alt_sprite.x = int(self._turtle_x - self._turtle_pic[0] // 2) + self._turtle_alt_sprite.y = int(self._turtle_y - self._turtle_pic[1] // 2) ########################################################################### # Move and draw - def forward(self, distance): + def forward(self, distance: float) -> None: """Move the turtle forward by the specified distance, in the direction the turtle is headed. :param distance: how far to move (integer or float) """ p = self.pos() - angle = ( - self._angleOffset + self._angleOrient * self._heading - ) % self._fullcircle + angle = (self._angleOffset + self._angleOrient * self._heading) % self._fullcircle x1 = p[0] + math.sin(math.radians(angle)) * distance y1 = p[1] + math.cos(math.radians(angle)) * distance self.goto(x1, y1) fd = forward - def backward(self, distance): + def backward(self, distance: float) -> None: """Move the turtle backward by distance, opposite to the direction the turtle is headed. Does not change the turtle's heading. @@ -288,7 +359,7 @@ def backward(self, distance): bk = backward back = backward - def right(self, angle): + def right(self, angle: float) -> None: """Turn turtle right by angle units. (Units are by default degrees, but can be set via the degrees() and radians() functions.) Angle orientation depends on the turtle mode, see mode(). @@ -302,7 +373,7 @@ def right(self, angle): rt = right - def left(self, angle): + def left(self, angle: float) -> None: """Turn turtle left by angle units. (Units are by default degrees, but can be set via the degrees() and radians() functions.) Angle orientation depends on the turtle mode, see mode(). @@ -317,7 +388,9 @@ def left(self, angle): lt = left # pylint:disable=too-many-branches,too-many-statements - def goto(self, x1, y1=None): + def goto( + self, x1: Union[float, Vec2D, Tuple[float, float]], y1: Optional[float] = None + ) -> None: """If y1 is None, x1 must be a pair of coordinates or an (x, y) tuple Move turtle to an absolute position. If the pen is down, draw line. @@ -326,35 +399,38 @@ def goto(self, x1, y1=None): :param x1: a number or a pair of numbers :param y1: a number or None """ - if y1 is None: - y1 = x1[1] - x1 = x1[0] - x1 += self._w // 2 - y1 = self._h // 2 - y1 - x0 = self._x - y0 = self._y + yn: float = x1[1] if y1 is None else y1 # type: ignore + xn: float = x1[0] if y1 is None else x1 # type: ignore + xn += self._w // 2 + yn = self._h // 2 - yn if not self.isdown(): - self._x = x1 # woot, we just skip ahead - self._y = y1 + self._x = xn # woot, we just skip ahead + self._y = yn self._drawturtle() return - steep = abs(y1 - y0) > abs(x1 - x0) + + self._do_draw_line(round(self._x), round(self._y), round(xn), round(yn)) + self._x = xn + self._y = yn + + def _do_draw_line(self, x0: int, y0: int, xn: int, yn: int): + steep = abs(yn - y0) > abs(xn - x0) rev = False - dx = x1 - x0 + dx = xn - x0 if steep: x0, y0 = y0, x0 - x1, y1 = y1, x1 - dx = x1 - x0 + xn, yn = yn, xn + dx = xn - x0 - if x0 > x1: + if x0 > xn: rev = True - dx = x0 - x1 + dx = x0 - xn - dy = abs(y1 - y0) + dy = abs(yn - y0) err = dx / 2 ystep = -1 - if y0 < y1: + if y0 < yn: ystep = 1 step = 1 if self._speed > 0: @@ -362,21 +438,21 @@ def goto(self, x1, y1=None): else: ts = 0 - while (not rev and x0 <= x1) or (rev and x1 <= x0): + while (not rev and x0 <= xn) or (rev and xn <= x0): if steep: try: self._plot(int(y0), int(x0), self._pencolor) except IndexError: pass - self._x = y0 - self._y = x0 + self._turtle_x = y0 + self._turtle_y = x0 else: try: self._plot(int(x0), int(y0), self._pencolor) except IndexError: pass - self._x = x0 - self._y = y0 + self._turtle_x = x0 + self._turtle_y = y0 if self._speed > 0: if step >= self._speed: # mark the step @@ -397,9 +473,10 @@ def goto(self, x1, y1=None): setpos = goto setposition = goto + # pylint:enable=too-many-branches,too-many-statements - def setx(self, x): + def setx(self, x: float) -> None: """Set the turtle's first coordinate to x, leave second coordinate unchanged. @@ -408,7 +485,7 @@ def setx(self, x): """ self.goto(x, self.pos()[1]) - def sety(self, y): + def sety(self, y: float) -> None: """Set the turtle's second coordinate to y, leave first coordinate unchanged. @@ -417,7 +494,7 @@ def sety(self, y): """ self.goto(self.pos()[0], y) - def setheading(self, to_angle): + def setheading(self, to_angle: float) -> None: """Set the orientation of the turtle to to_angle. Here are some common directions in degrees: @@ -437,7 +514,7 @@ def setheading(self, to_angle): seth = setheading - def home(self): + def home(self) -> None: """Move turtle to the origin - coordinates (0,0) - and set its heading to its start-orientation (which depends on the mode, see mode()). @@ -446,7 +523,7 @@ def home(self): self.goto(0, 0) # pylint:disable=too-many-locals, too-many-statements, too-many-branches - def _plot(self, x, y, c): + def _plot(self, x: float, y: float, c: int) -> None: if self._pensize == 1: try: self._fg_bitmap[int(x), int(y)] = c @@ -454,9 +531,7 @@ def _plot(self, x, y, c): except IndexError: pass r = self._pensize // 2 + 1 - angle = ( - self._angleOffset + self._angleOrient * self._heading - 90 - ) % self._fullcircle + angle = (self._angleOffset + self._angleOrient * self._heading - 90) % self._fullcircle sin = math.sin(math.radians(angle)) cos = math.cos(math.radians(angle)) x0 = x + sin * r @@ -526,7 +601,9 @@ def _plot(self, x, y, c): # pylint:enable=too-many-locals, too-many-statements, too-many-branches - def circle(self, radius, extent=None, steps=None): + def circle( + self, radius: float, extent: Optional[float] = None, steps: Optional[int] = None + ) -> None: """Draw a circle with given radius. The center is radius units left of the turtle; extent - an angle - determines which part of the circle is drawn. If extent is not given, draw the entire circle. If extent is not @@ -547,6 +624,12 @@ def circle(self, radius, extent=None, steps=None): # --or: circle(radius, extent) # arc # --or: circle(radius, extent, steps) # --or: circle(radius, steps=6) # 6-sided polygon + change_back = False + if not self._in_degrees(): + change_back = True + original_mode = "standard" if not self._logomode else "logo" + self.degrees() + self.mode("standard") pos = self.pos() h = self._heading if extent is None: @@ -568,11 +651,13 @@ def circle(self, radius, extent=None, steps=None): # get back to exact same position and heading self.goto(pos) self.setheading(h) + if change_back: + self.radians() + self.mode(original_mode) # pylint:disable=inconsistent-return-statements - def speed(self, speed=None): + def speed(self, speed: Optional[int] = None) -> Optional[int]: """ - Set the turtle's speed to an integer value in the range 0..10. If no argument is given, return current speed. @@ -599,10 +684,11 @@ def speed(self, speed=None): self._speed = 0 else: self._speed = speed + return None # pylint:enable=inconsistent-return-statements - def dot(self, size=None, color=None): + def dot(self, size: Optional[int] = None, color: Optional[int] = None) -> None: """Draw a circular dot with diameter size, using color. If size is not given, the maximum of pensize+4 and 2*pensize is used. @@ -611,6 +697,13 @@ def dot(self, size=None, color=None): :param color: the color of the dot """ + change_back = False + if not self._in_degrees(): + change_back = True + original_mode = "standard" if not self._logomode else "logo" + print(f"old mode: {original_mode}") + self.degrees() + self.mode("standard") if size is None: size = max(self._pensize + 4, self._pensize * 2) if color is None: @@ -634,8 +727,15 @@ def dot(self, size=None, color=None): self._pensize = 1 self._plot(self._x, self._y, color) self._pensize = pensize - - def stamp(self, bitmap=None, palette=None): + if change_back: + self.radians() + self.mode(original_mode) + + def stamp( + self, + bitmap: Optional[displayio.Bitmap] = None, + palette: Optional[displayio.Palette] = None, + ) -> int: """ Stamp a copy of the turtle shape onto the canvas at the current turtle position. Return a stamp_id for that stamp, which can be used to @@ -678,7 +778,7 @@ def stamp(self, bitmap=None, palette=None): return s_id - def clearstamp(self, stampid): + def clearstamp(self, stampid: int) -> None: """ Delete stamp with given stampid. @@ -700,9 +800,8 @@ def clearstamp(self, stampid): else: raise TypeError("Stamp id must be an int") - def clearstamps(self, n=None): + def clearstamps(self, n: Optional[int] = None) -> None: """ - Delete all or first/last n of turtle's stamps. If n is None, delete all stamps, if n > 0 delete first n stamps, else if n < 0 delete last n stamps. @@ -711,7 +810,7 @@ def clearstamps(self, n=None): """ i = 1 - for sid in self._stamps: # pylint: disable=consider-using-dict-items + for sid in self._stamps: if self._stamps[sid] is not None: self.clearstamp(sid) if n is not None and i >= n: @@ -721,13 +820,15 @@ def clearstamps(self, n=None): ########################################################################### # Tell turtle's state - def pos(self): + def pos(self) -> Vec2D: """Return the turtle's current location (x,y) (as a Vec2D vector).""" return Vec2D(self._x - self._w // 2, self._h // 2 - self._y) position = pos - def towards(self, x1, y1=None): + def towards( + self, x1: Union[float, Vec2D, Tuple[float, float]], y1: Optional[float] = None + ) -> float: """ Return the angle between the line from turtle position to position specified by (x,y) or the vector. This depends on the turtle's start @@ -746,21 +847,23 @@ def towards(self, x1, y1=None): result /= self._degreesPerAU return (self._angleOffset + self._angleOrient * result) % self._fullcircle - def xcor(self): + def xcor(self) -> float: """Return the turtle's x coordinate.""" return self._x - self._w // 2 - def ycor(self): + def ycor(self) -> float: """Return the turtle's y coordinate.""" return self._h // 2 - self._y - def heading(self): + def heading(self) -> float: """Return the turtle's current heading (value depends on the turtle mode, see mode()). """ return self._heading - def distance(self, x1, y1=None): + def distance( + self, x1: Union[float, List[float], Tuple[float, float]], y1: Optional[float] + ) -> float: """ Return the distance from the turtle to (x,y) or the vector, in turtle step units. @@ -769,16 +872,16 @@ def distance(self, x1, y1=None): :param y: a number if x is a number, else None """ - if y1 is None: - y1 = x1[1] - x1 = x1[0] - x0, y0 = self.pos() - return math.sqrt((x0 - x1) ** 2 + (y0 - y1) ** 2) + yn: float = x1[1] if y1 is None else y1 # type: ignore + xn: float = x1[0] if y1 is None else x1 # type: ignore + p = self.pos() + x0, y0 = (p[0], p[1]) + return math.sqrt((x0 - xn) ** 2 + (y0 - yn) ** 2) ########################################################################### # Setting and measurement - def _setDegreesPerAU(self, fullcircle): + def _setDegreesPerAU(self, fullcircle: float) -> None: """Helper function for degrees() and radians()""" self._fullcircle = fullcircle self._degreesPerAU = 360 / fullcircle @@ -787,7 +890,7 @@ def _setDegreesPerAU(self, fullcircle): else: self._angleOffset = -fullcircle / 4 - def degrees(self, fullcircle=360): + def degrees(self, fullcircle: float = 360) -> None: """Set angle measurement units, i.e. set number of "degrees" for a full circle. Default value is 360 degrees. @@ -796,12 +899,16 @@ def degrees(self, fullcircle=360): """ self._setDegreesPerAU(fullcircle) - def radians(self): + def _in_degrees(self) -> bool: + print(self._degreesPerAU) + return self._degreesPerAU == 1.0 + + def radians(self) -> None: """Set the angle measurement units to radians. Equivalent to degrees(2*math.pi).""" self._setDegreesPerAU(2 * math.pi) - def mode(self, mode=None): + def mode(self, mode: Optional[str] = None) -> Optional[str]: """ Set turtle mode ("standard" or "logo") and perform reset. @@ -828,12 +935,12 @@ def mode(self, mode=None): raise RuntimeError("Mode must be 'logo', 'standard', or None") return None - def window_height(self): + def window_height(self) -> float: """ Return the height of the turtle window.""" return self._h - def window_width(self): + def window_width(self) -> float: """ Return the width of the turtle window.""" return self._w @@ -841,25 +948,25 @@ def window_width(self): ########################################################################### # Drawing state - def pendown(self): + def pendown(self) -> None: """Pull the pen down - drawing when moving.""" self._penstate = True pd = pendown down = pendown - def penup(self): + def penup(self) -> None: """Pull the pen up - no drawing when moving.""" self._penstate = False pu = penup up = penup - def isdown(self): + def isdown(self) -> bool: """Return True if pen is down, False if it's up.""" return self._penstate - def pensize(self, width=None): + def pensize(self, width: Optional[int] = None) -> int: """ Set the line thickness to width or return it. If no argument is given, the current pensize is returned. @@ -878,12 +985,12 @@ def pensize(self, width=None): # pylint:disable=no-self-use - def _color_to_pencolor(self, c): + def _color_to_pencolor(self, c: int) -> int: return Color.colors.index(c) # pylint:enable=no-self-use - def pencolor(self, c=None): + def pencolor(self, c: Optional[int] = None) -> int: """ Return or set the pencolor. @@ -911,7 +1018,7 @@ def pencolor(self, c=None): self._turtle_palette.make_opaque(1) return c - def bgcolor(self, c=None): + def bgcolor(self, c: Optional[int] = None) -> int: """ Return or set the background color. @@ -945,7 +1052,7 @@ def bgcolor(self, c=None): return Color.colors[self._bg_color] # pylint:disable=inconsistent-return-statements - def bgpic(self, picname=None): + def bgpic(self, picname: Optional[str] = None) -> Optional[str]: """Set background image or return name of current backgroundimage. Optional argument: picname -- a string, name of an image file or "nopic". @@ -973,13 +1080,14 @@ def bgpic(self, picname=None): # centered self._odb_tilegrid.y = ((self._h * self._fg_scale) // 2) - (odb.height // 2) self._odb_tilegrid.x = ((self._w * self._fg_scale) // 2) - (odb.width // 2) + return None # pylint:enable=inconsistent-return-statements ########################################################################### # More drawing control - def reset(self): + def reset(self) -> None: """ Delete the turtle's drawings from the screen, re-center the turtle and set variables to the default values.""" @@ -993,7 +1101,7 @@ def reset(self): self.pensize(1) self.pencolor(Color.WHITE) - def clear(self): + def clear(self) -> None: """Delete the turtle's drawings from the screen. Do not move turtle.""" self.clearstamps() for w in range(self._w): @@ -1008,7 +1116,7 @@ def clear(self): ########################################################################### # Visibility - def showturtle(self): + def showturtle(self) -> None: """ Make the turtle visible.""" if self._turtle_group: @@ -1020,7 +1128,7 @@ def showturtle(self): st = showturtle - def hideturtle(self): + def hideturtle(self) -> None: """ Make the turtle invisible.""" if not self._turtle_group: @@ -1029,7 +1137,7 @@ def hideturtle(self): ht = hideturtle - def isvisible(self): + def isvisible(self) -> bool: """ Return True if the Turtle is shown, False if it's hidden.""" if self._turtle_group: @@ -1037,7 +1145,11 @@ def isvisible(self): return False # pylint:disable=too-many-statements, too-many-branches - def changeturtle(self, source=None, dimensions=(12, 12)): + def changeturtle( + self, + source: Optional[Union[displayio.TileGrid, str]] = None, + dimensions: Tuple[int, int] = (12, 12), + ) -> None: """ Change the turtle. if a string is provided, its a path to an image opened via OnDiskBitmap @@ -1102,16 +1214,14 @@ def changeturtle(self, source=None, dimensions=(12, 12)): self._turtle_group.append(self._turtle_alt_sprite) self._drawturtle() else: - raise TypeError( - 'Argument must be "str", a "displayio.TileGrid" or nothing.' - ) + raise TypeError('Argument must be "str", a "displayio.TileGrid" or nothing.') # pylint:enable=too-many-statements, too-many-branches ########################################################################### # Other - def _turn(self, angle): + def _turn(self, angle: float) -> None: if angle % self._fullcircle == 0: return if not self.isdown() or self._pensize == 1: @@ -1119,9 +1229,7 @@ def _turn(self, angle): self._heading %= self._fullcircle # wrap return start_angle = self._heading - steps = math.ceil( - (self._pensize * 2) * 3.1415 * (abs(angle) / self._fullcircle) - ) + steps = math.ceil((self._pensize * 2) * 3.1415 * (abs(angle) / self._fullcircle)) if steps < 1: d_angle = angle steps = 1 @@ -1142,7 +1250,7 @@ def _turn(self, angle): return if abs(angle - steps * d_angle) >= abs(d_angle): - steps += abs(angle - steps * d_angle) // abs(d_angle) + steps += int(abs(angle - steps * d_angle) // abs(d_angle)) self._plot(self._x, self._y, self._pencolor) for _ in range(steps): @@ -1156,7 +1264,7 @@ def _turn(self, angle): self._heading %= self._fullcircle self._plot(self._x, self._y, self._pencolor) - def _GCD(self, a, b): + def _GCD(self, a: int, b: int) -> int: """GCD(a,b): recursive 'Greatest common divisor' calculus for int numbers a and b""" if b == 0: diff --git a/docs/api.rst b/docs/api.rst index db048c0..31d0e34 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,5 +4,8 @@ .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) .. use this format as the module name: "adafruit_foo.foo" +API Reference +############# + .. automodule:: adafruit_turtle :members: diff --git a/docs/conf.py b/docs/conf.py index faff217..f4402aa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- - # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # # SPDX-License-Identifier: MIT +import datetime import os import sys @@ -16,6 +15,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinxcontrib.jquery", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.todo", @@ -37,7 +37,6 @@ "adafruit_bitmap_font", "adafruit_display_text", "adafruit_esp32spi", - "secrets", "adafruit_sdcard", "storage", "adafruit_io", @@ -66,7 +65,12 @@ # General information about the project. project = "Adafruit turtle Library" -copyright = "2019 Adafruit" +creation_year = "2019" +current_year = str(datetime.datetime.now().year) +year_duration = ( + current_year if current_year == creation_year else creation_year + " - " + current_year +) +copyright = year_duration + " Adafruit" author = "Adafruit" # The version info for the project you're documenting, acts as replacement for @@ -115,19 +119,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -on_rtd = os.environ.get("READTHEDOCS", None) == "True" - -if not on_rtd: # only import and set the theme if we're building docs locally - try: - import sphinx_rtd_theme - - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] - except: - html_theme = "default" - html_theme_path = ["."] -else: - html_theme_path = ["."] +import sphinx_rtd_theme + +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/requirements.txt b/docs/requirements.txt index 88e6733..979f568 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,4 +2,6 @@ # # SPDX-License-Identifier: Unlicense -sphinx>=4.0.0 +sphinx +sphinxcontrib-jquery +sphinx-rtd-theme diff --git a/examples/turtle_benzene.py b/examples/turtle_benzene.py index 5c1eb91..d9c470e 100644 --- a/examples/turtle_benzene.py +++ b/examples/turtle_benzene.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import Color, turtle turtle = turtle(board.DISPLAY) diff --git a/examples/turtle_bgpic_changeturtle.py b/examples/turtle_bgpic_changeturtle.py index e461854..e2b0d91 100644 --- a/examples/turtle_bgpic_changeturtle.py +++ b/examples/turtle_bgpic_changeturtle.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import Color, turtle turtle = turtle(board.DISPLAY) diff --git a/examples/turtle_circle.py b/examples/turtle_circle.py index 51c111e..d4ca9aa 100644 --- a/examples/turtle_circle.py +++ b/examples/turtle_circle.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import Color, turtle turtle = turtle(board.DISPLAY) diff --git a/examples/turtle_circle_hex.py b/examples/turtle_circle_hex.py index 5f02a4a..af4f0b5 100644 --- a/examples/turtle_circle_hex.py +++ b/examples/turtle_circle_hex.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import turtle turtle = turtle(board.DISPLAY) diff --git a/examples/turtle_dots.py b/examples/turtle_dots.py index 3baa449..9297ad8 100644 --- a/examples/turtle_dots.py +++ b/examples/turtle_dots.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import turtle print("Turtle time! Lets draw a square with dots") diff --git a/examples/turtle_hilbert.py b/examples/turtle_hilbert.py index c2a5e79..8427712 100644 --- a/examples/turtle_hilbert.py +++ b/examples/turtle_hilbert.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import turtle diff --git a/examples/turtle_koch.py b/examples/turtle_koch.py index a2d5ee8..1e5b681 100644 --- a/examples/turtle_koch.py +++ b/examples/turtle_koch.py @@ -2,26 +2,28 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import turtle def f(side_length, depth, generation): - if depth != 0: - side = lambda: f(side_length / 3, depth - 1, generation + 1) - side() - turtle.left(60) - side() - turtle.right(120) - side() - turtle.left(60) - side() + if depth == 0: + turtle.forward(side_length) + return + side = lambda: f(side_length / 3, depth - 1, generation + 1) + side() + turtle.left(60) + side() + turtle.right(120) + side() + turtle.left(60) + side() turtle = turtle(board.DISPLAY) unit = min(board.DISPLAY.width / 3, board.DISPLAY.height / 4) top_len = unit * 3 -print(top_len) turtle.penup() turtle.goto(-1.5 * unit, unit) turtle.pendown() diff --git a/examples/turtle_manual_hex.py b/examples/turtle_manual_hex.py index 11e54a4..d407a6c 100644 --- a/examples/turtle_manual_hex.py +++ b/examples/turtle_manual_hex.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import turtle turtle = turtle(board.DISPLAY) diff --git a/examples/turtle_overlayed_koch.py b/examples/turtle_overlayed_koch.py index 05bfef0..662ae92 100644 --- a/examples/turtle_overlayed_koch.py +++ b/examples/turtle_overlayed_koch.py @@ -2,21 +2,24 @@ # SPDX-License-Identifier: MIT import board -from adafruit_turtle import turtle, Color -generation_colors = [Color.RED, Color.BLUE, Color.GREEN] +from adafruit_turtle import Color, turtle + +generation_colors = [Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW] def f(side_length, depth, generation): - if depth != 0: - side = lambda: f(side_length / 3, depth - 1, generation + 1) - side() - turtle.left(60) - side() - turtle.right(120) - side() - turtle.left(60) - side() + if depth == 0: + turtle.forward(side_length) + return + side = lambda: f(side_length / 3, depth - 1, generation + 1) + side() + turtle.left(60) + side() + turtle.right(120) + side() + turtle.left(60) + side() def snowflake(num_generations, generation_color): @@ -40,7 +43,7 @@ def snowflake(num_generations, generation_color): turtle.goto(-1.5 * unit, unit) turtle.pendown() -for generations in range(3): +for generations in range(4): snowflake(generations, generation_colors[generations]) turtle.right(120) diff --git a/examples/turtle_sierpinski.py b/examples/turtle_sierpinski.py index ff9e155..c01f039 100644 --- a/examples/turtle_sierpinski.py +++ b/examples/turtle_sierpinski.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import turtle @@ -10,7 +11,6 @@ def getMid(p1, p2): def triangle(points, depth): - turtle.penup() turtle.goto(points[0][0], points[0][1]) turtle.pendown() diff --git a/examples/turtle_simpletest.py b/examples/turtle_simpletest.py index 7a0c68e..6f1681a 100644 --- a/examples/turtle_simpletest.py +++ b/examples/turtle_simpletest.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import Color, turtle turtle = turtle(board.DISPLAY) diff --git a/examples/turtle_square.py b/examples/turtle_square.py index 6d9486f..0a3aaaf 100644 --- a/examples/turtle_square.py +++ b/examples/turtle_square.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import Color, turtle turtle = turtle(board.DISPLAY) diff --git a/examples/turtle_star.py b/examples/turtle_star.py index 7a0c68e..6f1681a 100644 --- a/examples/turtle_star.py +++ b/examples/turtle_star.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import board + from adafruit_turtle import Color, turtle turtle = turtle(board.DISPLAY) diff --git a/examples/turtle_swirl.py b/examples/turtle_swirl.py index fab441e..62ca218 100644 --- a/examples/turtle_swirl.py +++ b/examples/turtle_swirl.py @@ -2,7 +2,8 @@ # SPDX-License-Identifier: MIT import board -from adafruit_turtle import turtle, Color + +from adafruit_turtle import Color, turtle turtle = turtle(board.DISPLAY) diff --git a/pyproject.toml b/pyproject.toml index f8e5813..fcb3b34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ requires = [ [project] name = "adafruit-circuitpython-turtle" description = "Turtle graphics library for CircuitPython and displayio" -version = "0.0.0-auto.0" +version = "0.0.0+auto.0" readme = "README.rst" authors = [ {name = "Adafruit Industries", email = "circuitpython@adafruit.com"} diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..f732478 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +target-version = "py38" +line-length = 100 + +[lint] +preview = true +select = ["I", "PL", "UP"] + +extend-select = [ + "D419", # empty-docstring + "E501", # line-too-long + "W291", # trailing-whitespace + "PLC0414", # useless-import-alias + "PLC2401", # non-ascii-name + "PLC2801", # unnecessary-dunder-call + "PLC3002", # unnecessary-direct-lambda-call + "E999", # syntax-error + "PLE0101", # return-in-init + "F706", # return-outside-function + "F704", # yield-outside-function + "PLE0116", # continue-in-finally + "PLE0117", # nonlocal-without-binding + "PLE0241", # duplicate-bases + "PLE0302", # unexpected-special-method-signature + "PLE0604", # invalid-all-object + "PLE0605", # invalid-all-format + "PLE0643", # potential-index-error + "PLE0704", # misplaced-bare-raise + "PLE1141", # dict-iter-missing-items + "PLE1142", # await-outside-async + "PLE1205", # logging-too-many-args + "PLE1206", # logging-too-few-args + "PLE1307", # bad-string-format-type + "PLE1310", # bad-str-strip-call + "PLE1507", # invalid-envvar-value + "PLE2502", # bidirectional-unicode + "PLE2510", # invalid-character-backspace + "PLE2512", # invalid-character-sub + "PLE2513", # invalid-character-esc + "PLE2514", # invalid-character-nul + "PLE2515", # invalid-character-zero-width-space + "PLR0124", # comparison-with-itself + "PLR0202", # no-classmethod-decorator + "PLR0203", # no-staticmethod-decorator + "UP004", # useless-object-inheritance + "PLR0206", # property-with-parameters + "PLR0904", # too-many-public-methods + "PLR0911", # too-many-return-statements + "PLR0912", # too-many-branches + "PLR0913", # too-many-arguments + "PLR0914", # too-many-locals + "PLR0915", # too-many-statements + "PLR0916", # too-many-boolean-expressions + "PLR1702", # too-many-nested-blocks + "PLR1704", # redefined-argument-from-local + "PLR1711", # useless-return + "C416", # unnecessary-comprehension + "PLR1733", # unnecessary-dict-index-lookup + "PLR1736", # unnecessary-list-index-lookup + + # ruff reports this rule is unstable + #"PLR6301", # no-self-use + + "PLW0108", # unnecessary-lambda + "PLW0120", # useless-else-on-loop + "PLW0127", # self-assigning-variable + "PLW0129", # assert-on-string-literal + "B033", # duplicate-value + "PLW0131", # named-expr-without-context + "PLW0245", # super-without-brackets + "PLW0406", # import-self + "PLW0602", # global-variable-not-assigned + "PLW0603", # global-statement + "PLW0604", # global-at-module-level + + # fails on the try: import typing used by libraries + #"F401", # unused-import + + "F841", # unused-variable + "E722", # bare-except + "PLW0711", # binary-op-exception + "PLW1501", # bad-open-mode + "PLW1508", # invalid-envvar-default + "PLW1509", # subprocess-popen-preexec-fn + "PLW2101", # useless-with-lock + "PLW3301", # nested-min-max +] + +ignore = [ + "PLR2004", # magic-value-comparison + "UP030", # format literals + "PLW1514", # unspecified-encoding + "PLR0913", # too-many-arguments + "PLR0915", # too-many-statements + "PLR0917", # too-many-positional-arguments + "PLR0904", # too-many-public-methods + "PLR0912", # too-many-branches + "PLR0916", # too-many-boolean-expressions + "PLR6301", # could-be-static no-self-use + "PLC0415", # import outside toplevel + "PLC2701", # private import + "UP007", # x | y typing + "UP006", # builtin instead of typing import + "PLR0914", # too many locals +] + +[format] +line-ending = "lf"