Repository: jg-rp/liquid (language: python)

Issue:
Hatch, Ruff, pyproject.toml and Markdown
Now that `pyproject.toml` is supported by [GitHub's dependency graph](https://github.com/orgs/community/discussions/6456), it seems like a good time to move this project away from the following:

- `setup.cfg`
- Pipienv
- Tox
- pylint
- flake8
- reStructuredText formatted README and CHANGELOG

And adopt:

- `pyproject.toml` (for all project configuration)
- [Hatch](https://hatch.pypa.io/latest/)
- [Ruff](https://beta.ruff.rs/docs/)
- Markdown
Hatch, Ruff, pyproject.toml and Markdown
Now that `pyproject.toml` is supported by [GitHub's dependency graph](https://github.com/orgs/community/discussions/6456), it seems like a good time to move this project away from the following:

- `setup.cfg`
- Pipienv
- Tox
- pylint
- flake8
- reStructuredText formatted README and CHANGELOG

And adopt:

- `pyproject.toml` (for all project configuration)
- [Hatch](https://hatch.pypa.io/latest/)
- [Ruff](https://beta.ruff.rs/docs/)
- Markdown


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
index 92c72903..2409e5b7 100644
--- a/.github/workflows/coverage.yaml
+++ b/.github/workflows/coverage.yaml
@@ -1,21 +1,17 @@
-name: Coverage
+name: coverage
 on: [push, pull_request]
 
 jobs:
-  coverage:
+  lint:
     runs-on: ubuntu-latest
-
     steps:
       - uses: actions/checkout@v3
       - name: Set up Python 3.10
         uses: actions/setup-python@v4
         with:
-          python-version: '3.10'
-          cache: 'pip'
-      - run: pip install tox coverage
-      - name: combine coverage
+          python-version: "3.10"
+      - name: Install dependencies
         run: |
-          python -m coverage erase
-          python -m tox -c tox_cov.ini
-          python -m coverage combine
-          python -m coverage report --fail-under=98
+          python -m pip install --upgrade pip
+          python -m pip install --upgrade hatch
+      - run: hatch run cov
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 590d78fe..7c0b776a 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -1,16 +1,17 @@
-name: Lint
+name: lint
 on: [push, pull_request]
 
 jobs:
   lint:
     runs-on: ubuntu-latest
-
     steps:
       - uses: actions/checkout@v3
       - name: Set up Python 3.10
         uses: actions/setup-python@v4
         with:
-          python-version: '3.10'
-          cache: 'pip'
-      - run: pip install tox
-      - run: python -m tox -e lint
+          python-version: "3.10"
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install --upgrade hatch
+      - run: hatch run lint
diff --git a/.github/workflows/typing.yaml b/.github/workflows/typing.yaml
index d9f3a9c3..632a9a0b 100644
--- a/.github/workflows/typing.yaml
+++ b/.github/workflows/typing.yaml
@@ -1,22 +1,17 @@
-name: Typing
+name: typing
 on: [push, pull_request]
 
 jobs:
-  typing:
+  lint:
     runs-on: ubuntu-latest
-
     steps:
       - uses: actions/checkout@v3
       - name: Set up Python 3.10
         uses: actions/setup-python@v4
         with:
-          python-version: '3.10'
-          cache: 'pip'
-      - name: cache mypy
-        uses: actions/cache@v3
-        with:
-          path: ./.mypy_cache
-          key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }}
-        if: matrix.tox == 'typing'
-      - run: pip install tox
-      - run: python -m tox -e typing
+          python-version: "3.10"
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install --upgrade hatch
+      - run: hatch run typing
diff --git a/.gitignore b/.gitignore
index 9da33d0e..3c43542e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,6 +51,7 @@ coverage.xml
 
 # Sphinx documentation
 docs/_build/
+site/
 
 # PyBuilder
 target/
diff --git a/.prettierignore b/.prettierignore
deleted file mode 100644
index 2a616051..00000000
--- a/.prettierignore
+++ /dev/null
@@ -1,1 +0,0 @@
-*.yaml
\ No newline at end of file
diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index 461287d6..00000000
--- a/.pylintrc
+++ /dev/null
@@ -1,596 +0,0 @@
-[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-allow-list=
-
-# 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. (This is an alternative name to extension-pkg-allow-list
-# for backward compatibility.)
-extension-pkg-whitelist=
-
-# Return non-zero exit code if any of these messages/categories are detected,
-# even if score is above --fail-under value. Syntax same as enable. Messages
-# specified are enabled, while categories only check already-enabled messages.
-fail-on=
-
-# Specify a score threshold to be exceeded before program exits with error.
-fail-under=10.0
-
-# Files or directories to be skipped. 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 paths and can be in Posix or Windows format.
-ignore-paths=
-
-# Files or directories matching the regex patterns are skipped. The regex
-# matches against base names, not paths. The default value ignores emacs file
-# locks
-ignore-patterns=^\.#
-
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
-# number of processors available to use.
-jobs=1
-
-# Control the amount of potential inferred values when inferring a single
-# object. This can help the performance when dealing with large functions or
-# complex, nested conditions.
-limit-inference-results=100
-
-# List of plugins (as comma separated values of python module names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# Minimum Python version to use for version dependent checks. Will default to
-# the version used to run pylint.
-py-version=3.10
-
-# Discover python modules and packages in the file system subtree.
-recursive=no
-
-# When enabled, pylint would attempt to guess common misconfiguration and emit
-# user-friendly hints instead of false-positive error messages.
-suggestion-mode=yes
-
-# 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, CONTROL_FLOW, 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 re-enable 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=raw-checker-failed,
-        bad-inline-option,
-        locally-disabled,
-        file-ignored,
-        suppressed-message,
-        useless-suppression,
-        deprecated-pragma,
-        use-symbolic-message-instead,
-        duplicate-code,
-        cyclic-import,
-
-# 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=c-extension-no-member
-
-
-[REPORTS]
-
-# Python expression which should return a score less than or equal to 10. You
-# have access to the variables 'fatal', 'error', 'warning', 'refactor',
-# 'convention', and 'info' which contain the number of messages in each
-# category, as well as 'statement' which is the total number of statements
-# analyzed. This score is used by the global evaluation report (RP0004).
-evaluation=max(0, 0 if fatal else 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, e.g.
-# 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
-
-# Complete name of functions that never returns. When checking for
-# inconsistent-return-statements if a never returning function is called then
-# it will be considered as an explicit return statement and no message will be
-# printed.
-never-returning-functions=sys.exit,argparse.parse_error
-
-
-[SIMILARITIES]
-
-# Comments are removed from the similarity computation
-ignore-comments=yes
-
-# Docstrings are removed from the similarity computation
-ignore-docstrings=yes
-
-# Imports are removed from the similarity computation
-ignore-imports=no
-
-# Signatures are removed from the similarity computation
-ignore-signatures=no
-
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-
-[VARIABLES]
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid defining new builtins when possible.
-additional-builtins=
-
-# Tells whether unused global variables should be treated as a violation.
-allow-global-unused-variables=yes
-
-# List of names allowed to shadow builtins
-allowed-redefined-builtins=
-
-# 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. expected to
-# not be 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,past.builtins,future.builtins,builtins,io
-
-
-[BASIC]
-
-# Naming style matching correct argument names.
-argument-naming-style=snake_case
-
-# Regular expression matching correct argument names. Overrides argument-
-# naming-style. If left empty, argument names will be checked with the set
-# naming style.
-#argument-rgx=
-
-# Naming style matching correct attribute names.
-attr-naming-style=snake_case
-
-# Regular expression matching correct attribute names. Overrides attr-naming-
-# style. If left empty, attribute names will be checked with the set naming
-# style.
-#attr-rgx=
-
-# Bad variable names which should always be refused, separated by a comma.
-bad-names=foo,
-          bar,
-          baz,
-          toto,
-          tutu,
-          tata
-
-# Bad variable names regexes, separated by a comma. If names match any regex,
-# they will always be refused
-bad-names-rgxs=
-
-# Naming style matching correct class attribute names.
-class-attribute-naming-style=any
-
-# Regular expression matching correct class attribute names. Overrides class-
-# attribute-naming-style. If left empty, class attribute names will be checked
-# with the set naming style.
-#class-attribute-rgx=
-
-# Naming style matching correct class constant names.
-class-const-naming-style=UPPER_CASE
-
-# Regular expression matching correct class constant names. Overrides class-
-# const-naming-style. If left empty, class constant names will be checked with
-# the set naming style.
-#class-const-rgx=
-
-# Naming style matching correct class names.
-class-naming-style=PascalCase
-
-# Regular expression matching correct class names. Overrides class-naming-
-# style. If left empty, class names will be checked with the set naming style.
-#class-rgx=
-
-# Naming style matching correct constant names.
-const-naming-style=UPPER_CASE
-
-# Regular expression matching correct constant names. Overrides const-naming-
-# style. If left empty, constant names will be checked with the set naming
-# style.
-#const-rgx=
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=-1
-
-# Naming style matching correct function names.
-function-naming-style=snake_case
-
-# Regular expression matching correct function names. Overrides function-
-# naming-style. If left empty, function names will be checked with the set
-# naming style.
-#function-rgx=
-
-# Good variable names which should always be accepted, separated by a comma.
-good-names=i,
-           j,
-           k,
-           ex,
-           Run,
-           _,
-           ch,
-           fd,
-           n,
-           rv,  # result value
-           op,  # operation
-           ip,  # instruction pointer
-           vm,  # virtual machine
-           it,  # iterator
-           sp,  # stack pointer
-
-# Good variable names regexes, separated by a comma. If names match any regex,
-# they will always be accepted
-good-names-rgxs=
-
-# Include a hint for the correct naming format with invalid-name.
-include-naming-hint=no
-
-# Naming style matching correct inline iteration names.
-inlinevar-naming-style=any
-
-# Regular expression matching correct inline iteration names. Overrides
-# inlinevar-naming-style. If left empty, inline iteration names will be checked
-# with the set naming style.
-#inlinevar-rgx=
-
-# Naming style matching correct method names.
-method-naming-style=snake_case
-
-# Regular expression matching correct method names. Overrides method-naming-
-# style. If left empty, method names will be checked with the set naming style.
-#method-rgx=
-
-# Naming style matching correct module names.
-module-naming-style=snake_case
-
-# Regular expression matching correct module names. Overrides module-naming-
-# style. If left empty, module names will be checked with the set naming style.
-#module-rgx=
-
-# 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.
-# These decorators are taken in consideration only for invalid-name.
-property-classes=abc.abstractproperty
-
-# Regular expression matching correct type variable names. If left empty, type
-# variable names will be checked with the set naming style.
-#typevar-rgx=
-
-# Naming style matching correct variable names.
-variable-naming-style=snake_case
-
-# Regular expression matching correct variable names. Overrides variable-
-# naming-style. If left empty, variable names will be checked with the set
-# naming style.
-#variable-rgx=
-
-
-[FORMAT]
-
-# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
-expected-line-ending-format=
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )?<?https?://\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
-
-# 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
-
-
-[SPELLING]
-
-# Limits count of emitted suggestions for spelling mistakes.
-max-spelling-suggestions=4
-
-# Spelling dictionary name. Available dictionaries: none. To make it work,
-# install the 'python-enchant' package.
-spelling-dict=
-
-# List of comma separated words that should be considered directives if they
-# appear and the beginning of a comment and should not be checked.
-spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
-
-# List of comma separated words that should not be checked.
-spelling-ignore-words=
-
-# A path to a file that contains the private dictionary; one word per line.
-spelling-private-dict-file=
-
-# Tells whether to store unknown words to the private dictionary (see the
-# --spelling-private-dict-file option) instead of raising a message.
-spelling-store-unknown-words=no
-
-
-[STRING]
-
-# This flag controls whether inconsistent-quotes generates a warning when the
-# character used as a quote delimiter is used inconsistently within a module.
-check-quote-consistency=no
-
-# This flag controls whether the implicit-str-concat should generate a warning
-# on implicit string concatenation in sequences defined over several lines.
-check-str-concat-over-line-jumps=no
-
-
-[LOGGING]
-
-# The type of string formatting that logging methods do. `old` means using %
-# formatting, `new` is for `{}` formatting.
-logging-format-style=old
-
-# Logging modules to check that the string format arguments are in logging
-# function parameter format.
-logging-modules=logging
-
-
-[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
-# class is considered mixin if its name matches the mixin-class-rgx option.
-ignore-mixin-members=yes
-
-# Tells whether to warn about missing members when the owner of the attribute
-# is inferred to be None.
-ignore-none=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=
-
-# 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
-
-# Regex pattern to define which classes are considered mixins ignore-mixin-
-# members is set to 'yes'
-mixin-class-rgx=.*[Mm]ixin
-
-# List of decorators that change the signature of a decorated function.
-signature-mutators=
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=FIXME,
-      XXX,
-      TODO
-
-# Regular expression of note tags to take in consideration.
-#notes-rgx=
-
-
-[IMPORTS]
-
-# List of modules that can be imported at any level, not just the top level
-# one.
-allow-any-import-level=
-
-# 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=
-
-# Output a graph (.gv or any supported image format) of external dependencies
-# to the given file (report RP0402 must not be disabled).
-ext-import-graph=
-
-# Output a graph (.gv or any supported image format) of all (i.e. internal and
-# external) dependencies to the given file (report RP0402 must not be
-# disabled).
-import-graph=
-
-# Output a graph (.gv or any supported image format) of internal dependencies
-# to 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
-
-# Couples of modules and preferred modules, separated by a comma.
-preferred-modules=
-
-
-[DESIGN]
-
-# List of regular expressions of class ancestor names to ignore when counting
-# public methods (see R0903)
-exclude-too-few-public-methods=
-
-# List of qualified class names to ignore when counting class parents (see
-# R0901)
-ignored-parents=
-
-# Maximum number of arguments for function / method.
-max-args=7
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=7
-
-# Maximum number of boolean expressions in an if statement (see R0916).
-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=2
-
-
-[CLASSES]
-
-# Warn about protected attribute access inside special methods
-check-protected-access-in-special-methods=no
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,
-                      __new__,
-                      setUp,
-                      __post_init__
-
-# 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=cls
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "BaseException, Exception".
-overgeneral-exceptions=builtins.BaseException,
-                       builtins.Exception
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index ef9ddc6f..d51e0159 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -5,12 +5,16 @@
 # Required
 version: 2
 
-# Build documentation in the docs/ directory with Sphinx
-sphinx:
-   configuration: docs/conf.py
+# Set the version of Python and other tools you might need
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.10"
 
-# Optionally set the version of Python and requirements required to build your docs
+mkdocs:
+  configuration: mkdocs.yml
+
+# Optionally declare the Python requirements required to build your docs
 python:
-   version: 3.8
-   install:
-   - requirements: docs/requirements.txt
\ No newline at end of file
+  install:
+    - requirements: docs/requirements.txt
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 00000000..7f3db90b
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,1 @@
+Python Liquid's change log has moved to `CHANGES.md <https://github.com/jg-rp/liquid/blob/main/CHANGES.md>`_.
\ No newline at end of file
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 00000000..97912e97
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,408 @@
+# Python Liquid Change Log
+
+## Version 1.9.0 (unreleased)
+
+**Fixes**
+
+- Changed `FileSystemLoader` so that loaded templates can be pickled. See [#107](https://github.com/jg-rp/liquid/issues/107).
+- Fixed the `liquid.Environment` template cache. Now, when given a `cache_size` of `0`, the cache is disabled. See [#108](https://github.com/jg-rp/liquid/issues/108).
+
+**Features**
+
+- Added context-aware template loader bootstrapping methods. See [#109](https://github.com/jg-rp/liquid/pull/109).
+
+## Version 1.8.1
+
+**Fixes**
+
+- Removed unnecessary wrapping of inline conditional expressions in `BooleanExpression`.
+- Fixed async evaluation of macro arguments. Previously they were always being evaluated synchronously.
+- Allow `macro` names to be quoted or unquoted. Quoted and unquoted macro names are now equivalent when defining and/or calling a macro.
+
+**Compatibility**
+
+- Support bracketed variables without a leading identifier. See [Shopify/liquid #1680](https://github.com/Shopify/liquid/pull/1680).
+- Allow whitespace control from `raw` tags. See [Shopify/liquid #1683](https://github.com/Shopify/liquid/pull/1683).
+
+## Version 1.8.0
+
+**Features**
+
+- Added `{% extends %}` and `{% block %}` tags for template inheritance. These are extra tags that need to be registered with a `liquid.Environment` explicitly. ([docs](https://jg-rp.github.io/liquid/extra/tags#extends--block), [source](https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/extends.py))
+- Added a new `sort_numeric` filter. `sort_numeric` returns a new list with items from the input sequence sorted by any integers and/or floats found in the string representation of each item. ([docs](https://jg-rp.github.io/liquid/extra/filters#sort_numeric), [source](https://github.com/jg-rp/liquid/blob/main/liquid/extra/filters/array.py#L35))
+
+**Fixes**
+
+- Fixed a bug with the `cycle` tag when using `liquid.future.Environment`. We were misinterpreting unquoted cycle group names as strings rather than variables to be resolved, and not Liquid stringifying some cycled items before output. We've also rolled back changes to `CycleNode.children()` from version 1.7.0.
+- Fixed a regression bug that lead to some erroneous filtered expressions tokens to be silently ignored. Specifically any tokens that appear after a valid left value and the first filter or end of expression. We now raise a `LiquidSyntaxError` in such cases. See [#103](https://github.com/jg-rp/liquid/issues/103).
+- Fixed parenthesized conditional expression syntax error reporting. We now raise a `LiquidSyntaxError` when given unbalanced parentheses. See [#101](https://github.com/jg-rp/liquid/issues/101).
+
+**Compatibility**
+
+- The `{% for %}` tag now accepts a string literal as its iterable. Unlike Shopify/liquid, whether a string literal or a variable resolving to a string, the default `Environment` will iterate over characters in the string. `liquid.future.Environment` is now consistent with Shopify/liquid, in that it iterates over an "array" where the first an only item is the string. See [#102](https://github.com/jg-rp/liquid/issues/102).
+- The `round` filter is now consistent with Shopify/liquid and Ruby 3 when given non-integer arguments. See [Shopify/liquid#1590](https://github.com/Shopify/liquid/issues/1590).
+
+## Version 1.7.0
+
+**Fixes**
+
+- Fixed counting of "local" variables (those created with `assign`, `capture`, etc.) in templates rendered with the `{% render %}` tag during contextual analysis. Previously these variables were not reported in the results of `BoundTemplate.analyze_with_context()`. See [#92](https://github.com/jg-rp/liquid/issues/92).
+- Both [#43](https://github.com/jg-rp/liquid/issues/43) and [#90](https://github.com/jg-rp/liquid/issues/90) have been fixed with the introduction of `liquid.future.Environment`, an environment that aims for maximum compatibility with Ruby Liquid, without concern for backwards incompatible changes to template rendering behavior. `liquid.Environment` should be considered the most stable "standard" configuration, `liquid.future.Environment` sacrifices stability for improved compatibility with Ruby Liquid. See the [known issues page](https://jg-rp.github.io/liquid/known_issues).
+- Fixed a bug with AST traversal of `cycle` nodes. Previously, `CycleNode.children()` erroneously included a `cycle group name` expression, if available.
+
+**Features**
+
+- Report template filter usage as well as variable usage with `BoundTemplate.analyze()` and `BoundTemplate.analyze_with_context()`. See [#91](https://github.com/jg-rp/liquid/pull/91) and [docs](https://jg-rp.github.io/liquid/guides/static-template-analysis#filters).
+- Report template tag usage when statically analyzing a template with `BoundTemplate.analyze()`. See [#97](https://github.com/jg-rp/liquid/pull/97) and [docs](https://jg-rp.github.io/liquid/guides/static-template-analysis#tags).
+- Analyze template tags using `Environment.analyze_tags()`. This form of tag analysis happens before a template is fully parsed, giving us the opportunity to find unknown, unexpected and unbalanced tags that might cause the parser to raise an exception or skip template blocks. See [#98](https://github.com/jg-rp/liquid/pull/98) and [docs](https://jg-rp.github.io/liquid/guides/tag-analysis).
+
+## Version 1.6.1
+
+**Fixes**
+
+- Fixed static template analysis fails with `{% break %}` and `{% continue %}`. See [#89](https://github.com/jg-rp/liquid/issues/89).
+
+## Version 1.6.0
+
+**Fixes**
+
+- Fixed the string representation of `liquid.expression.Identifier`, which is exposed in the results of `liquid.BoundTemplate.analyze()`. We now represent variable path elements containing a `.` as quoted strings inside square brackets. See [#87](https://github.com/jg-rp/liquid/issues/87).
+
+**Features**
+
+- The dictionaries returned by `liquid.BoundTemplate.analyze()` now use instances of `ReferencedVariable` for their keys. `ReferencedVariable` is a `str` subclass that adds a `parts` property, being a tuple representation of the variable. See [#87](https://github.com/jg-rp/liquid/issues/87).
+
+## Version 1.5.1
+
+**Fixes**
+
+- Fixed `case` and `when` tag expression parsing. `when` expressions no longer fail when presented with a string containing a comma. Handling of comma and `or` separated "sub-expressions" is now consistent with the reference implementation. See [#86](https://github.com/jg-rp/liquid/issues/86).
+
+## Version 1.5.0
+
+**Features**
+
+The following non-standard tags and filters are reimplementations of those found in the
+[Python Liquid Extra project](https://github.com/jg-rp/liquid-extra), which is now
+receiving bugfix updates only. Unlike standard tags and filters, which are registered
+for you automatically, extra tags and filters must be explicitly registered with an
+Environment`. See https://jg-rp.github.io/liquid/extra/introduction.
+
+- Added an `if` tag that supports a logical `not` operator and grouping terms with parentheses. ([docs](https://jg-rp.github.io/liquid/extra/tags#if-not), [source](https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/if_not.py))
+
+- Added drop-in replacements for the standard output statement, `assign` tag and `echo` tag that support inline conditional expressions. ([docs](https://jg-rp.github.io/liquid/extra/tags#inline-if--else), [source](https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/if_expressions.py))
+
+- Added `macro` and `call` tags that define parameterized Liquid snippets for reuse. ([docs](https://jg-rp.github.io/liquid/extra/tags#macro--call), [source](https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/macro.py))
+
+- Added the `with` tag that extends the local namespace with block scoped variables. ([docs](https://jg-rp.github.io/liquid/extra/tags#with), [source](https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/_with.py))
+
+- Added the `json`, `index`, `script_tag` and `stylesheet_tag` filters. ([docs](https://jg-rp.github.io/liquid/extra/filters), [source](https://github.com/jg-rp/liquid/tree/main/liquid/extra/filters))
+
+**Compatibility**
+
+- `for` tag arguments can now be separated by commas as well as whitespace. See [Shopify/liquid#1658](https://github.com/Shopify/liquid/pull/1658)
+
+## Version 1.4.7
+
+**Hot fix**
+
+- Fixed a bug where use of a [local namespace limit](https://jg-rp.github.io/liquid/guides/resource-limits#local-namespace-limit) would raise a `TypeError` when unhashable types were found in a render context's local namespace. See [#79](https://github.com/jg-rp/liquid/issues/79).
+
+## Version 1.4.6
+
+**Fixes**
+
+- The `tablerowloop` drop now exposes its `row` property. See [#77](https://github.com/jg-rp/liquid/issues/77).
+- `for` and `tablerow` tag arguments can now be string representations of integers as well as integer literals and variables that resolve to integers. See [#78](https://github.com/jg-rp/liquid/issues/78).
+
+**Compatibility**
+
+- The `truncatewords` filter no longer raises a `FilterArgumentError` if its argument is greater than `2147483648` and the number of words in the input string is less than the target number of words. This is inline with recent changes committed to the reference implementation of Liquid.
+- The `slice` filter now clamps its arguments to between `-9223372036854775808` and `9223372036854775807`, as does the reference implementation of Liquid.
+
+## Version 1.4.5
+
+**Hot fix**
+
+- Fixed a bug where boolean expressions and the default filter would treat `0.0` and `decimal.Decimal("0")` as `False`. Python considers these values to be falsy, Liquid does not. See [#74](https://github.com/jg-rp/liquid/issues/74).
+- Future-proof str to int conversion limit. We will now use `sys.get_int_max_str_digits` if it is available and `LIQUIDINTMAXSTRDIGITS` is not set. Note that `sys.get_int_max_str_digits` is called once at startup, so Liquid's limit will change with `sys.set_int_max_str_digits`.
+
+## Version 1.4.4
+
+**Fixes**
+
+- Keep comment text for later static analysis when parsing `{% comment %}` block tags. See [#70](https://github.com/jg-rp/liquid/issues/70).
+- Guard against DoS by very large str to int conversion. See https://github.com/python/cpython/issues/95778. ([docs](https://jg-rp.github.io/liquid/guides/resource-limits#string-to-integer-limit))
+
+## Version 1.4.3
+
+**Fixes**
+
+- Updated the built-in `date` filter to support parsing UNIX timestamps from integers and string representations of integers. For consistency with the reference implementation of Liquid, `date` now returns the input string unchanged if it can not be parsed. See [#67](https://github.com/jg-rp/liquid/issues/67).
+- Fixed an issue with the "loop iteration" resource limit. It was failing to carry an existing loop counter more than one level deep when using `Context.copy()`. See [#68](https://github.com/jg-rp/liquid/issues/68).
+
+## Version 1.4.2
+
+**Fixes**
+
+- Fixed a potential memory leak from using `functools.lru_cache` on a class method. See [#63](https://github.com/jg-rp/liquid/issues/63).
+- Fixed a bug with the `default` filter. Liquid zero should not be equal to `False`. The `default` filter now returns `0` if its left value is zero. Before it would have return its default value. See [#62](https://github.com/jg-rp/liquid/issues/62).
+- Fixed a bug where boolean expressions would consider Liquid `0` and `false` to be equal and `0` to be falsy. Python Liquid is now consistent with the reference implementation when comparing integers to booleans. See [#65](https://github.com/jg-rp/liquid/issues/65).
+
+## Version 1.4.1
+
+**Hot fix**
+
+- Fixed a bug with the `StrictDefaultFilter`. It was failing to be strict when accessed by some filter decorators and helpers. Now the `default` filter will immediately return its default value if its left value defines a `force_liquid_default` property and that property is truthy. See [#62](https://github.com/jg-rp/liquid/issues/62).
+
+## Version 1.4.0
+
+**Features**
+
+- `StrictDefaultUndefined`, an undefined type that plays nicely with the `default` filter, is now built in. ([docs](https://jg-rp.github.io/liquid/guides/undefined-variables#the-default-filter))
+- Configure resource limits with class attributes set on a Liquid `Environment`. Those class attributes are `context_depth_limit`, `loop_iteration_limit`, `local_namespace_limit` and `output_stream_limit`. ([docs](https://jg-rp.github.io/liquid/guides/resource-limits))
+
+**Fixes**
+
+- Fixed a bug in `StrictUndefined` that, when extended, stopped if from looking at its own `msg` property. See [#57](https://github.com/jg-rp/liquid/issues/57).
+
+## Version 1.3.0
+
+**Features**
+
+- Allow render context customization by subclassing `Context` and `BoundTemplate`.
+- Contextual template analysis with `BoundTemplate.analyze_with_context()`. Complementing static template analysis, released in version 1.2.0, contextual template analysis performs a template `render`, capturing information about template variable usage as it goes. ([docs](https://jg-rp.github.io/liquid/guides/contextual-template-analysis))
+
+## Version 1.2.1
+
+- Add `typing-extensions` dependency.
+
+## Version 1.2.0
+
+**Features**
+
+- New inline comment tag `{% # .. %}`. See [Shopify Liquid PR #1498](https://github.com/Shopify/liquid/pull/1498)
+- Template static analysis. `BoundTemplate.analyze()` and `BoundTemplate.analyze_async()` traverse a template's abstract syntax tree and report template variable usage. Static tree traversal (without rendering or evaluating expressions) is supported by the new, optional `children()` methods of `liquid.expression.Expression` and `liquid.ast.Node`. ([docs](https://jg-rp.github.io/liquid/guides/static-template-analysis))
+
+**Fixes**
+
+- Fixed a bug where the lexer would incorrectly calculate an expression's line number if there were one or more newlines between a tag name and it's expression. Most notable with `liquid` tags where it is common to put a newline immediately after "liquid".
+- More robust syntax error handling when parsing Liquid expression filters. The refactored expression lexers from version 1.1.3 failed to account for some classes of syntax error.
+
+## Version 1.1.7
+
+- Fixed a bug where double pipe characters (`||`) in a filtered expression would cause an `IndexError`. A `LiquidSyntaxError` is now raised in such cases, including the line number of the offending error.
+- Changed `Environment.fromString` to catch unexpected parsing errors. A Liquid `Error` will now be raised with a message of "unexpected liquid parsing error" and its `__cause__` set to the offending exception.
+
+## Version 1.1.6
+
+- Fixed a bug where the `where` filter would incorrectly ignore an explicit `false` given as the target value. See #51.
+
+## Version 1.1.5
+
+- Prioritise object properties and keys named `size`, `first` and `last` over the special built-in properties of the same names. See #46.
+- Fixed a bug with the `uniq` filter. It no longer raises an exception when given a key argument and a sequence containing objects that don't have that key/property. See #47.
+- The `strip_html` filter now removes `style` and `script` tag blocks in their entirety, including everything in between. See #45.
+- Added `remove_last` and `replace_last` filters.
+
+## Version 1.1.4
+
+- Lazy `forloop` helper variables. Don't calculate `index`, `rindex` etc. unless accessed.
+- Implemented `forloop.name`, as per the reference implementation. `forloop.name` is the concatenation of the loop variable identifier and the target iterable identifier, or a string representation of a range literal, separated by a hyphen.
+- Fixed a bug with the `divided_by` filter. Given a float value and integer argument, it was incorrectly doing integer division.
+- Simplified `tablerowloop` and `tablerow` HTML generation.
+
+## Version 1.1.3
+
+- Refactored expression lexers. New, subtly different, tag expression tokenizers are now in `liquid.expressions`. Built-in tags use these lexers indirectly via new specialized expression parsers. Older expression lexers and parsers will be maintained until at least Python Liquid version 2.0 for those that use them in custom tags. See #42.
+- Specialized expression parsers. Each of the three built-in expression types now have a dedicated parser defined in `liquid.expressions`, whereas before all expression parsing went through `liquid.parse.ExpressionParser.parse_expression()`. Built-in tags now use these new parsers. The more general parser will be maintained until at least Python Liquid Version 2.0. See #42.
+- `liquid.parse.Parser.parse_block()` now accepts any container as its `end` argument. Benchmarks show that using a `frozenset` for `end` instead of a tuple gives a small performance improvement.
+- Fixed an incompatibility with the reference implementation where Python Liquid would not recognize identifiers with a trailing question mark. This seems to be a common idiom in Ruby to indicate something returns a Boolean value.
+- Added `get_source_with_context()` and `get_source_with_context_async()` to `liquid.loaders.BaseLoader`. Custom loaders can now use the active render context to dynamically modify their search space when used from `include` or `render`, or any custom tag using `Context.get_template_with_context()`.
+  `Context.get_template_with_context()` also accepts arbitrary keyword arguments that are passed along to `get_source_with_context()`. The build-in `include` and `render` tags add a `tag` argument with their tag name, so custom loaders can modify their search space depending on which tag was used.
+  See the [Custom Loaders](https://jg-rp.github.io/liquid/guides/custom-loaders) documentation for examples.
+
+## Version 1.1.2
+
+- Fixed a bug where a for loop's limit would be incorrect when using `offset: continue` multiple times (three or more `for` tags looping over the same sequence). See #41.
+
+## Version 1.1.1
+
+- Fixed a bug where blocks that contain whitespace only were being suppressed when the whitespace was explicitly output. Automatic whitespace suppression now only occurs in `if`, `unlesss` and `for` blocks that don't contain an output statement or `echo` tag, even if the output itself is whitespace. See #38.
+- Fixed a bug where the behavior of the special `.first` and `.last` properties did not match that of the `first` and `last` filters. Now, if given a string, `.first` and `.last` will return an undefined, and the `first` and `last` filters will return `None`. See #34.
+
+## Version 1.1.0
+
+- Added new comment syntax. Disabled by default, enable shorthand comments with the `template_comments` argument to `liquid.Template` or `liquid.Environment`. When `True`, anything between `{#` and `#}` will be considered a comment.
+- New expression cache. Distinct from the existing template cache, optionally cache common Liquid expression types (conditions, loops and filtered expressions) to avoid lexing and parsing the same expression multiple times.
+- Fixed a bug where, in some cases, `forloop.length` would be incorrect when using `offsset: continue` in a loop expression.
+
+## Version 1.0.4
+
+- A range literal will now use a default of `0` rather than raising a `LiquidTypeError` if either its start or stop values can't be cast to an integer.
+- Gracefully handle `liquid` tags that are empty or only contain whitespace.
+- Gracefully handle empty `echo` tags.
+
+## Version 1.0.3
+
+- Explicit re-export
+- Changed `Context._tag_namespace` to `Context.tag_namespace`.
+
+## Version 1.0.2
+
+- Fixed manifest error.
+
+## Version 1.0.1
+
+- Added `py.typed`
+
+## Version 1.0.0
+
+Version bump. First stable release.
+
+## Version 0.11.1
+
+- Template loaders can now include additional template meta data using the `matter` argument of `liquid.loaders.TemplateSource`. See the example `FrontMatterFileSystemLoader` in the README. See #32.
+- Implemented `ChoiceLoader`, a loader that tries to load a template from a list of loaders.
+- Added a `FileExtensionLoader`, a template loader that inherits from `FileSystemLoader` and automatically appends a file extension if one is missing.
+- The built-in `date` filter now accepts the special input value of "today" as well as "now".
+- The built-in `truncate` filter now has a default length of 50.
+- The built-in `truncatewords` filter now has a default number of words of 15.
+- Fixed a bug with the `slice` filter where it would return an empty string when presented with a negative start index and length that would have exceeded the length of the sequence. See #35.
+- Drops can now define safe HTML string representations using a `__html__` method.
+- Removed `liquid.mode.error()` in favour of `liquid.Environment.error()`
+
+## Version 0.11.0
+
+The following behavioral changes are the result of feedback gained from exporting Python
+Liquid's "golden" test cases, and running them against Ruby Liquid (the reference
+implementation). Both Python Liquid version 0.11.0 and Ruby Liquid version 5.1.0 pass
+all tests currently defined in `liquid/golden/`.
+
+- Added support for comma separated `when` expressions. See #31.
+- The built-in `join`, `concat`, `where`, `uniq` and `compact` filters now use the new `sequence_filter` decorator. `sequence_filter` coerces filter left values to array-like objects. `sequence_filter` will also flatten nested array-like objects, just like the reference implementation.
+- The built-in `first`, `last` and `map` filters now operate on any array-like objects. Previously they were limited to lists and tuples. Strings still don't work.
+- The built-in `uniq` and `compact` filters now accept an optional argument. If an argument is provided, it should be the name of a property and the left value should be a sequence of objects.
+- The `size` filter now returns a default of `0` if its left value does not have a `__len__` method.
+- The `replace` and `replace_first` filters now treat undefined arguments as an empty string.
+- The `slice` filter now works on lists, tuples and ranges, as well as strings.
+- Fixed a bug where the `math_filter` decorator would cast strings representations of negative integers to a float rather than an int.
+- Added golden test cases for all filters.
+
+## Version 0.10.2
+
+- Moved and organized "render" test cases into importable "golden" test cases.
+- Change `RangeLiteral` string representation to match the reference implementation.
+- Add newlines to `tablerow` output. As per the reference implementation.
+
+## Version 0.10.1
+
+- Range literals can now be assigned, compared and passed as arguments to `include` or `render` tags. They can also be filtered as if they were an array.
+- Range literals will accept a float value for start and/or stop values. If a float is given for start or stop, it will be cast to an integer.
+- Fixed a bug where the token stream would return the wrong token when peeking immediately after a push.
+
+## Version 0.10.0
+
+- Changed named counter (`increment` and `decrement`) scoping. Unless a named counter is shadowed by an `assign` or `capture`, the counter will be in scope for all subsequent Liquid expressions.
+- Changed `{% increment %}` to be a post-increment operation. `{% decrement %}` remains a pre-decrement operation.
+- Added `forloop.parentloop`. Access parent `forloop` objects from nested loops.
+
+## Version 0.9.1
+
+- `unless` blocks can now contain `else` and `elsif` blocks.
+- Added support for array item access with negative indices. Closes #27.
+- Improved error messages for context lookups that resulting an `IndexError` or `KeyError`.
+
+## Version 0.9.0
+
+- Fixed a bug where arguments to `Template()` where not being passed to the implicit environment properly (again).
+- Fixed a bug where some errors from the `sort` and `map` filters were being ignored. Those filters can now raise a `FilterError`.
+- Removed depreciated class-based filters.
+
+## Version 0.8.1
+
+- Removed `@abstractmethod` from `liquid.loaders.BaseLoader.get_source`. Custom loaders are now free to implement either `get_source` or `get_source_async` or both. The `BaseLoader` implementation of `get_source` simply raises a `NotImplementedError`.
+- `liquid.loaders.TemplateSource.uptodate` (as returned by `get_source` and `get_source_async`) can now be a coroutine function. This means async loaders can check a template's source for changes asynchronously.
+- Added the `cache_size` argument to `Environment` and `Template` for controlling the capacity of the default template cache.
+- Easier subclassing of `liquid.parser.ExpressionParser` with `END_EXPRESSION`.
+
+## Version 0.8.0
+
+Version bump. Last release before removing depreciated class-based filters.
+
+## Version 0.7.8
+
+- The `default` filter now uses `__liquid__`, if available, when testing an object for truthy-ness.
+
+## Version 0.7.7
+
+- Recursive use of the "render" tag now raises a `ContextDepthError` if `MAX_CONTEXT_DEPTH` is exceeded. This is now consistent with recursive "include".
+- Drops (custom classes in a render context) can now mimic primitive Liquid values when used as array indexes or hash keys, or in conditional expressions (including `case`/`when` and `unless`). If defined, the result of calling a drop's `__liquid__` method will be used in those scenarios.
+- Added `base64_encode`, `base64_decode`, `base64_url_safe_encode` and `base64_url_safe_decode` filters.
+- Added asynchronous template loading and rendering. When `Template.render_async` is awaited, `render` and `include` tags will load templates asynchronously. Custom loaders should implement `get_source_async`.
+- Added support for asynchronous drops. If a class implements `__getitem_async__`, which is assumed to be an async version of `__getitem__`, it will be awaited instead of calling `__getitem__`.
+
+## Version 0.7.6
+
+- Class-based filters are now depreciated in favour of decorated filter functions. Abstract filter classes (such as `liquid.builtin.filters.string.StringFilter`) will be removed in Liquid 0.9.
+- All built-in filters are now implemented as decorated functions. Legacy, class-based filters are no longer registered automatically and will be removed in Liquid 0.9.
+- Legacy filter "helpers" are now depreciated and will be removed in Liquid 0.9. Use the new decorators, like `liquid.filter.string_filter` and `liquid.filter.with_context`, instead.
+- The `block` argument to the `liquid.ast.ConditionalBlockNode` constructor is no longer optional.
+
+## Version 0.7.5
+
+- Auto reload. Disable automatic reloading of templates by passing `auto_reload=False` to `Environment` or `Template`. For deployments where template sources don't change between service reloads, setting auto_reload to `False` can yield an increase in performance by avoiding calls to `uptodate`.
+- Fixed a bug where, when using the `Template` API, `undefined`, `strict_filters`, and `autoescape` where not always passed through to the implicit environment correctly.
+- Added support for continuing a for loop using `offset: continue`. See #14.
+
+## Version 0.7.4
+
+.. \_MarkupSafe: https://github.com/pallets/markupsafe
+
+- HTML auto-escaping. If `MarkupSafe`\_ is installed and the `autoescape` argument to `Environment` or `Template` is `True`, context variables will be HTML escaped automatically.
+- `LiquidSyntaxError` exceptions now include the offending template source in the `source` property.
+
+## Version 0.7.3
+
+- The built-in `FileSystemLoader` now accepts a list of paths to search, in order, returning the first template source found. The `search_path` argument can be a string or path-like, or an iterable of strings or path-like objects.
+- Added the `encoding` argument to `FileSystemLoader`. Files will be opened with the given encoding. Defaults to `"utf-8"`.
+- `FileSystemLoader` will raise a `TemplateNotFound` exception if `..` appears in a template name.
+
+## Version 0.7.2
+
+- Undefined variables are now represented by the `Undefined` type, or a subclass of `Undefined`. `Undefined` behaves like `nil`, but can also be iterated over and indexed without error.
+- Attempting to loop over an undefined variable no longer raises a `LiquidTypeError`.
+- Optionally pass `liquid.StrictUndefined` as the `undefined` argument to `Template()` or `Environment()` to render in `strict variables` mode. All operations on an instance of `StrictUndefined` raise an `UndefinedError`.
+- Filters can now raise `FilterValueError` in addition to `FilterArgumentError`. Where a `FilterValueError` refers to an issue with the left value a filter is applied to.
+- Applying a built-in filter to an undefined variable no longer raises a `FilterArgumentError` in most cases.
+- Added the `strict_filters` argument to the `Environment` and `Template` constructors. When `True`, the default, undefined filters raise a `NoSuchFilterFunc` exception at render time. When `False`, undefined filters are silently ignored.
+- The `join` filter now forces items in its left value to strings before joining them.
+- The `join` filter's argument is now optional, defaulting to a string containing a single space.
+
+## Version 0.7.0
+
+- New `Template` API. Create templates from strings without an `Environment`.
+- The `template` object is no longer included in every render context automatically.
+
+## Version 0.6.4
+
+- Implemented the `blank` keyword. Empty string and strings containing only whitespace are equal to `blank`.
+- Implemented the `null` keyword. `null` is an alias for `nil`.
+- Implemented the `ifchanged` tag.
+
+## Version 0.6.2
+
+- Refactored the standard expression parser. It's now possible to extend `ExpressionParser` for use with custom tags.
+- Decoupled boolean expression parsing for easier `if`, `unless` and `case` tag subclassing.
+
+## Version 0.6.1
+
+- Added support for Python 3.7 and PyPy3.7.
+
+## Version 0.6.0
+
+- Added support for named filter parameters.
+- The `default` filter now accepts the named parameter `allow_false`.
+- The `truncate_words` filter now forces a minimum number of words to 1.
+- The `newline_to_br` filter now replaces `\n` and `\r\n` with `<br />\n`.
+- The `strip_newlines` filter strips `\r\n`.
diff --git a/CHANGES.rst b/CHANGES.rst
deleted file mode 100644
index 468fcc36..00000000
--- a/CHANGES.rst
+++ /dev/null
@@ -1,711 +0,0 @@
-Python Liquid Change Log
-========================
-
-Version 1.9.0 (unreleased)
---------------------------
-
-**Fixes**
-
-- Changed ``FileSystemLoader`` so that loaded templates can be pickled.
-  See `#107 <https://github.com/jg-rp/liquid/issues/107>`_.
-- Fixed the ``liquid.Environment`` template cache. Now, when given a ``cache_size`` of
-  ``0``, the cache is disabled. See `#108 <https://github.com/jg-rp/liquid/issues/108>`_.
-
-**Features**
-
-- Added context-aware template loader bootstrapping methods.
-  See `#109 <https://github.com/jg-rp/liquid/pull/109>`_.
-
-Version 1.8.1
--------------
-
-**Fixes**
-
-- Removed unnecessary wrapping of inline conditional expressions in
-  ``BooleanExpression``.
-- Fixed async evaluation of macro arguments. Previously they were always being evaluated
-  synchronously.
-- Allow `macro` names to be quoted or unquoted. Quoted and unquoted macro names are
-  now equivalent when defining and/or calling a macro.
-
-**Compatibility**
-
-- Support bracketed variables without a leading identifier.
-  See `Shopify/liquid #1680 <https://github.com/Shopify/liquid/pull/1680>`_.
-- Allow whitespace control from ``raw`` tags.
-  See `Shopify/liquid #1683 <https://github.com/Shopify/liquid/pull/1683>`_.
-
-Version 1.8.0
--------------
-
-**Features**
-
-- Added ``{% extends %}`` and ``{% block %}`` tags for template inheritance. These are 
-  extra tags that need to be registered with a ``liquid.Environment`` explicitly. 
-  (`docs <https://jg-rp.github.io/liquid/extra/tags#extends--block>`_,
-  `source <https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/extends.py>`_)
-- Added a new ``sort_numeric`` filter. ``sort_numeric`` returns a new list with items
-  from the input sequence sorted by any integers and/or floats found in the string
-  representation of each item.
-  (`docs <https://jg-rp.github.io/liquid/extra/filters#sort_numeric>`_,
-  `source <https://github.com/jg-rp/liquid/blob/main/liquid/extra/filters/array.py#L35>`_)
-
-**Fixes**
-
-- Fixed a bug with the ``cycle`` tag when using ``liquid.future.Environment``. We were 
-  misinterpreting unquoted cycle group names as strings rather than variables to be
-  resolved, and not Liquid stringifying some cycled items before output. We've also
-  rolled back changes to ``CycleNode.children()`` from version 1.7.0.
-- Fixed a regression bug that lead to some erroneous filtered expressions tokens to be
-  silently ignored. Specifically any tokens that appear after a valid left value and the
-  first filter or end of expression. We now raise a ``LiquidSyntaxError`` in such
-  cases. See `#103 <https://github.com/jg-rp/liquid/issues/103>`_.
-- Fixed parenthesized conditional expression syntax error reporting. We now raise a 
-  ``LiquidSyntaxError`` when given unbalanced parentheses.
-  See `#101 <https://github.com/jg-rp/liquid/issues/101>`_.
-
-**Compatibility**
-
-- The ``{% for %}`` tag now accepts a string literal as its iterable. Unlike
-  Shopify/liquid, whether a string literal or a variable resolving to a string, the
-  default ``Environment`` will iterate over characters in the string.
-  ``liquid.future.Environment`` is now consistent with Shopify/liquid, in that it
-  iterates over an "array" where the first an only item is the string.
-  See `#102 <https://github.com/jg-rp/liquid/issues/102>`_.
-- The `round` filter is now consistent with Shopify/liquid and Ruby 3 when given
-  non-integer arguments.
-  See `Shopify/liquid#1590 <https://github.com/Shopify/liquid/issues/1590>`_.
-
-Version 1.7.0
--------------
-
-**Fixes**
-
-- Fixed counting of "local" variables (those created with ``assign``, ``capture``, etc.)
-  in templates rendered with the ``{% render %}`` tag during contextual analysis.
-  Previously these variables were not reported in the results of
-  ``BoundTemplate.analyze_with_context()``.
-  See `#92 <https://github.com/jg-rp/liquid/issues/92>`_.
-- Both `#43 <https://github.com/jg-rp/liquid/issues/43>`_ and 
-  `#90 <https://github.com/jg-rp/liquid/issues/90>`_ have been fixed with the
-  introduction of ``liquid.future.Environment``, an environment that aims for maximum
-  compatibility with Ruby Liquid, without concern for backwards incompatible changes to
-  template rendering behavior. ``liquid.Environment`` should be considered the most
-  stable "standard" configuration, ``liquid.future.Environment`` sacrifices stability
-  for improved compatibility with Ruby Liquid.
-  See the `known issues page <https://jg-rp.github.io/liquid/known_issues>`_.
-- Fixed a bug with AST traversal of ``cycle`` nodes. Previously, ``CycleNode.children()``
-  erroneously included a `cycle group name` expression, if available. 
-
-**Features**
-
-- Report template filter usage as well as variable usage with ``BoundTemplate.analyze()``
-  and ``BoundTemplate.analyze_with_context()``.
-  See `#91 <https://github.com/jg-rp/liquid/pull/91>`_ and
-  `docs <https://jg-rp.github.io/liquid/guides/static-template-analysis#filters>`_.
-- Report template tag usage when statically analyzing a template with
-  ``BoundTemplate.analyze()``. See `#97 <https://github.com/jg-rp/liquid/pull/97>`_ and
-  `docs <https://jg-rp.github.io/liquid/guides/static-template-analysis#tags>`_.
-- Analyze template tags using ``Environment.analyze_tags()``. This form of tag analysis
-  happens before a template is fully parsed, giving us the opportunity to find unknown,
-  unexpected and unbalanced tags that might cause the parser to raise an exception or
-  skip template blocks. See `#98 <https://github.com/jg-rp/liquid/pull/98>`_ and 
-  `docs <https://jg-rp.github.io/liquid/guides/tag-analysis>`_.
-
-Version 1.6.1
--------------
-
-**Fixes**
-
-- Fixed static template analysis fails with ``{% break %}`` and ``{% continue %}``.
-  See `#89 <https://github.com/jg-rp/liquid/issues/89>`_.
-
-Version 1.6.0
--------------
-
-**Fixes**
-
-- Fixed the string representation of ``liquid.expression.Identifier``, which is exposed
-  in the results of ``liquid.BoundTemplate.analyze()``. We now represent variable path
-  elements containing a ``.`` as quoted strings inside square brackets.
-  See `#87 <https://github.com/jg-rp/liquid/issues/87>`_.
-
-**Features**
-
-- The dictionaries returned by ``liquid.BoundTemplate.analyze()`` now use instances of
-  ``ReferencedVariable`` for their keys. ``ReferencedVariable`` is a ``str`` subclass
-  that adds a ``parts`` property, being a tuple representation of the variable.
-  See `#87 <https://github.com/jg-rp/liquid/issues/87>`_.
-
-Version 1.5.1
--------------
-
-**Fixes**
-
-- Fixed ``case`` and ``when`` tag expression parsing. ``when`` expressions no longer
-  fail when presented with a string containing a comma. Handling of comma and ``or``
-  separated "sub-expressions" is now consistent with the reference implementation.
-  See `#86 <https://github.com/jg-rp/liquid/issues/86>`_.
-
-Version 1.5.0
--------------
-
-**Features**
-
-The following non-standard tags and filters are reimplementations of those found in the
-`Python Liquid Extra project <https://github.com/jg-rp/liquid-extra>`_, which is now
-receiving bugfix updates only. Unlike standard tags and filters, which are registered
-for you automatically, extra tags and filters must be explicitly registered with an
-``Environment``. See https://jg-rp.github.io/liquid/extra/introduction.
-
-- Added an ``if`` tag that supports a logical ``not`` operator and grouping
-  terms with parentheses.
-  (`docs <https://jg-rp.github.io/liquid/extra/tags#if-not>`_,
-  `source <https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/if_not.py>`_)
-
-- Added drop-in replacements for the standard output statement, ``assign`` tag and
-  ``echo`` tag that support inline conditional expressions.
-  (`docs <https://jg-rp.github.io/liquid/extra/tags#inline-if--else>`_,
-  `source <https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/if_expressions.py>`_)
-
-- Added ``macro`` and ``call`` tags that define parameterized Liquid snippets for reuse.
-  (`docs <https://jg-rp.github.io/liquid/extra/tags#macro--call>`_,
-  `source <https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/macro.py>`_)
-
-- Added the ``with`` tag that extends the local namespace with block scoped variables.
-  (`docs <https://jg-rp.github.io/liquid/extra/tags#with>`_,
-  `source <https://github.com/jg-rp/liquid/blob/main/liquid/extra/tags/_with.py>`_)
-
-- Added the ``json``, ``index``, ``script_tag`` and ``stylesheet_tag`` filters.
-  (`docs <https://jg-rp.github.io/liquid/extra/filters>`_,
-  `source <https://github.com/jg-rp/liquid/tree/main/liquid/extra/filters>`_)
-
-**Compatibility**
-
-- ``for`` tag arguments can now be separated by commas as well as whitespace.
-  See `Shopify/liquid#1658 <https://github.com/Shopify/liquid/pull/1658>`_
-
-Version 1.4.7
--------------
-
-**Hot fix**
-
-- Fixed a bug where use of a `local namespace limit <https://jg-rp.github.io/liquid/guides/resource-limits#local-namespace-limit>`_
-  would raise a ``TypeError`` when unhashable types were found in a render context's
-  local namespace. See `#79 <https://github.com/jg-rp/liquid/issues/79>`_.
-
-Version 1.4.6
--------------
-
-**Fixes**
-
-- The ``tablerowloop`` drop now exposes its ``row`` property.
-  See `#77 <https://github.com/jg-rp/liquid/issues/77>`_.
-- ``for`` and ``tablerow`` tag arguments can now be string representations of integers
-  as well as integer literals and variables that resolve to integers.
-  See `#78 <https://github.com/jg-rp/liquid/issues/78>`_.
-
-**Compatibility**
-
-- The ``truncatewords`` filter no longer raises a ``FilterArgumentError`` if its
-  argument is greater than ``2147483648`` and the number of words in the input string
-  is less than the target number of words. This is inline with recent changes committed
-  to the reference implementation of Liquid.
-- The ``slice`` filter now clamps its arguments to between ``-9223372036854775808`` and
-  ``9223372036854775807``, as does the reference implementation of Liquid.
-
-Version 1.4.5
--------------
-
-**Hot fix**
-
-- Fixed a bug where boolean expressions and the default filter would treat ``0.0`` and 
-  ``decimal.Decimal("0")`` as ``False``. Python considers these values to be falsy,
-  Liquid does not. See `#74 <https://github.com/jg-rp/liquid/issues/74>`_.
-- Future-proof str to int conversion limit. We will now use ``sys.get_int_max_str_digits``
-  if it is available and ``LIQUIDINTMAXSTRDIGITS`` is not set. Note that ``sys.get_int_max_str_digits``
-  is called once at startup, so Liquid's limit will change with ``sys.set_int_max_str_digits``.
-
-Version 1.4.4
--------------
-
-**Fixes**
-
-- Keep comment text for later static analysis when parsing ``{% comment %}`` block tags.
-  See `#70 <https://github.com/jg-rp/liquid/issues/70>`_.
-- Guard against DoS by very large str to int conversion.
-  See https://github.com/python/cpython/issues/95778.
-  (`docs <https://jg-rp.github.io/liquid/guides/resource-limits#string-to-integer-limit>`_)
-
-Version 1.4.3
--------------
-
-**Fixes**
-
-- Updated the built-in ``date`` filter to support parsing UNIX timestamps from integers
-  and string representations of integers. For consistency with the reference
-  implementation of Liquid, ``date`` now returns the input string unchanged if it can
-  not be parsed. See `#67 <https://github.com/jg-rp/liquid/issues/67>`_.
-- Fixed an issue with the "loop iteration" resource limit. It was failing to carry an
-  existing loop counter more than one level deep when using ``Context.copy()``. See
-  `#68 <https://github.com/jg-rp/liquid/issues/68>`_.
-
-Version 1.4.2
--------------
-
-**Fixes**
-
-- Fixed a potential memory leak from using ``functools.lru_cache`` on a class method.
-  See `#63 <https://github.com/jg-rp/liquid/issues/63>`_.
-- Fixed a bug with the ``default`` filter. Liquid zero should not be equal to ``False``.
-  The ``default`` filter now returns ``0`` if its left value is zero. Before it would
-  have return its default value. See `#62 <https://github.com/jg-rp/liquid/issues/62>`_.
-- Fixed a bug where boolean expressions would consider Liquid ``0`` and ``false`` to be
-  equal and ``0`` to be falsy. Python Liquid is now consistent with the reference
-  implementation when comparing integers to booleans.
-  See `#65 <https://github.com/jg-rp/liquid/issues/65>`_.
-
-Version 1.4.1
--------------
-
-**Hot fix**
-
-- Fixed a bug with the ``StrictDefaultFilter``. It was failing to be strict when
-  accessed by some filter decorators and helpers. Now the ``default`` filter will
-  immediately return its default value if its left value defines a
-  ``force_liquid_default`` property and that property is truthy.
-  See `#62 <https://github.com/jg-rp/liquid/issues/62>`_.
-
-Version 1.4.0
--------------
-
-**Features**
-
-- ``StrictDefaultUndefined``, an undefined type that plays nicely with the ``default``
-  filter, is now built in. (`docs <https://jg-rp.github.io/liquid/guides/undefined-variables#the-default-filter>`_)
-- Configure resource limits with class attributes set on a Liquid ``Environment``. Those
-  class attributes are ``context_depth_limit``, ``loop_iteration_limit``,
-  ``local_namespace_limit`` and ``output_stream_limit``.
-  (`docs <https://jg-rp.github.io/liquid/guides/resource-limits>`_)
-
-**Fixes**
-
-- Fixed a bug in ``StrictUndefined`` that, when extended, stopped if from looking at its
-  own ``msg`` property. See `#57 <https://github.com/jg-rp/liquid/issues/57>`_.
-
-
-Version 1.3.0
--------------
-
-**Features**
-
-- Allow render context customization by subclassing ``Context`` and ``BoundTemplate``.
-- Contextual template analysis with ``BoundTemplate.analyze_with_context()``.
-  Complementing static template analysis, released in version 1.2.0, contextual template
-  analysis performs a template `render`, capturing information about template variable
-  usage as it goes. (`docs <https://jg-rp.github.io/liquid/guides/contextual-template-analysis>`_)
-
-
-Version 1.2.1
--------------
-
-- Add ``typing-extensions`` dependency.
-
-Version 1.2.0
--------------
-
-**Features**
-
-- New inline comment tag ``{% # .. %}``. See `Shopify Liquid PR #1498 <https://github.com/Shopify/liquid/pull/1498>`_
-- Template static analysis. ``BoundTemplate.analyze()`` and ``BoundTemplate.analyze_async()``
-  traverse a template's abstract syntax tree and report template variable usage. Static 
-  tree traversal (without rendering or evaluating expressions) is supported by the new,
-  optional ``children()`` methods of ``liquid.expression.Expression`` and ``liquid.ast.Node``.
-  (`docs <https://jg-rp.github.io/liquid/guides/static-template-analysis>`_)
-
-**Fixes**
-
-- Fixed a bug where the lexer would incorrectly calculate an expression's line number
-  if there were one or more newlines between a tag name and it's expression. Most 
-  notable with ``liquid`` tags where it is common to put a newline immediately after
-  "liquid".
-- More robust syntax error handling when parsing Liquid expression filters. The 
-  refactored expression lexers from version 1.1.3 failed to account for some classes of
-  syntax error.
-
-Version 1.1.7
--------------
-
-- Fixed a bug where double pipe characters (``||``) in a filtered expression would cause
-  an ``IndexError``. A ``LiquidSyntaxError`` is now raised in such cases, including the
-  line number of the offending error.
-- Changed ``Environment.fromString`` to catch unexpected parsing errors. A Liquid
-  ``Error`` will now be raised with a message of "unexpected liquid parsing error"
-  and its ``__cause__`` set to the offending exception.
-
-Version 1.1.6
--------------
-
-- Fixed a bug where the ``where`` filter would incorrectly ignore an explicit ``false``
-  given as the target value. See #51.
-
-Version 1.1.5
--------------
-
-- Prioritise object properties and keys named ``size``, ``first`` and ``last`` over the
-  special built-in properties of the same names. See #46.
-- Fixed a bug with the ``uniq`` filter. It no longer raises an exception when given a 
-  key argument and a sequence containing objects that don't have that key/property.
-  See #47.
-- The ``strip_html`` filter now removes ``style`` and ``script`` tag blocks in their
-  entirety, including everything in between. See #45.
-- Added ``remove_last`` and ``replace_last`` filters.
-
-Version 1.1.4
--------------
-
-- Lazy ``forloop`` helper variables. Don't calculate ``index``, ``rindex`` etc. unless
-  accessed.
-- Implemented ``forloop.name``, as per the reference implementation. ``forloop.name`` is
-  the concatenation of the loop variable identifier and the target iterable identifier,
-  or a string representation of a range literal, separated by a hyphen.
-- Fixed a bug with the ``divided_by`` filter. Given a float value and integer argument,
-  it was incorrectly doing integer division.
-- Simplified ``tablerowloop`` and ``tablerow`` HTML generation.
-
-Version 1.1.3
--------------
-
-- Refactored expression lexers. New, subtly different, tag expression tokenizers are now
-  in ``liquid.expressions``. Built-in tags use these lexers indirectly via new 
-  specialized expression parsers. Older expression lexers and parsers will be maintained
-  until at least Python Liquid version 2.0 for those that use them in custom tags.
-  See #42.
-- Specialized expression parsers. Each of the three built-in expression types now have a
-  dedicated parser defined in ``liquid.expressions``, whereas before all expression
-  parsing went through ``liquid.parse.ExpressionParser.parse_expression()``. Built-in
-  tags now use these new parsers. The more general parser will be maintained until at
-  least Python Liquid Version 2.0. See #42.
-- ``liquid.parse.Parser.parse_block()`` now accepts any container as its ``end``
-  argument. Benchmarks show that using a ``frozenset`` for ``end`` instead of a tuple
-  gives a small performance improvement.
-- Fixed an incompatibility with the reference implementation where Python Liquid would
-  not recognize identifiers with a trailing question mark. This seems to be a common
-  idiom in Ruby to indicate something returns a Boolean value.
-- Added ``get_source_with_context()`` and ``get_source_with_context_async()`` to 
-  ``liquid.loaders.BaseLoader``. Custom loaders can now use the active render context to
-  dynamically modify their search space when used from ``include`` or ``render``, or any
-  custom tag using ``Context.get_template_with_context()``.
-
-  ``Context.get_template_with_context()`` also accepts arbitrary keyword arguments that
-  are passed along to ``get_source_with_context()``. The build-in ``include`` and
-  ``render`` tags add a ``tag`` argument with their tag name, so custom loaders can 
-  modify their search space depending on which tag was used.
-
-  See the `Custom Loaders <https://jg-rp.github.io/liquid/guides/custom-loaders>`_
-  documentation for examples.
-
-Version 1.1.2
--------------
-
-- Fixed a bug where a for loop's limit would be incorrect when using
-  ``offset: continue`` multiple times (three or more ``for`` tags looping over the same
-  sequence). See #41.
-
-Version 1.1.1
--------------
- 
-- Fixed a bug where blocks that contain whitespace only were being suppressed when the
-  whitespace was explicitly output. Automatic whitespace suppression now only occurs in
-  ``if``, ``unlesss`` and ``for`` blocks that don't contain an output statement or
-  ``echo`` tag, even if the output itself is whitespace. See #38.
-- Fixed a bug where the behavior of the special ``.first`` and ``.last`` properties did
-  not match that of the ``first`` and ``last`` filters. Now, if given a string,
-  ``.first`` and ``.last`` will return an undefined, and the ``first`` and ``last``
-  filters will return ``None``. See #34.
-
-Version 1.1.0
--------------
-
-- Added new comment syntax. Disabled by default, enable shorthand comments with the
-  ``template_comments`` argument to ``liquid.Template`` or ``liquid.Environment``.
-  When ``True``, anything between ``{#`` and ``#}`` will be considered a comment.
-- New expression cache. Distinct from the existing template cache, optionally cache
-  common Liquid expression types (conditions, loops and filtered expressions) to
-  avoid lexing and parsing the same expression multiple times.
-- Fixed a bug where, in some cases, ``forloop.length`` would be incorrect when using
-  ``offsset: continue`` in a loop expression.
-
-Version 1.0.4
--------------
-
-- A range literal will now use a default of ``0`` rather than raising a
-  ``LiquidTypeError`` if either its start or stop values can't be cast to an integer.
-- Gracefully handle ``liquid`` tags that are empty or only contain whitespace.
-- Gracefully handle empty ``echo`` tags.
-
-Version 1.0.3
--------------
-
-- Explicit re-export
-- Changed ``Context._tag_namespace`` to ``Context.tag_namespace``.
-
-Version 1.0.2
--------------
-
-- Fixed manifest error.
-
-Version 1.0.1
--------------
-
-- Added ``py.typed``
-
-Version 1.0.0
--------------
-
-Version bump. First stable release.
-
-Version 0.11.1
---------------
-
-- Template loaders can now include additional template meta data using the ``matter``
-  argument of ``liquid.loaders.TemplateSource``. See the example
-  ``FrontMatterFileSystemLoader`` in the README. See #32.
-- Implemented ``ChoiceLoader``, a loader that tries to load a template from a list of
-  loaders.
-- Added a ``FileExtensionLoader``, a template loader that inherits from
-  ``FileSystemLoader`` and automatically appends a file extension if one is missing.
-- The built-in ``date`` filter now accepts the special input value of "today" as well as
-  "now".
-- The built-in ``truncate`` filter now has a default length of 50.
-- The built-in ``truncatewords`` filter now has a default number of words of 15.
-- Fixed a bug with the ``slice`` filter where it would return an empty string when
-  presented with a negative start index and length that would have exceeded the length
-  of the sequence. See #35.
-- Drops can now define safe HTML string representations using a ``__html__`` method.
-- Removed ``liquid.mode.error()`` in favour of ``liquid.Environment.error()``
-
-Version 0.11.0
---------------
-
-The following behavioral changes are the result of feedback gained from exporting Python
-Liquid's "golden" test cases, and running them against Ruby Liquid (the reference
-implementation). Both Python Liquid version 0.11.0 and Ruby Liquid version 5.1.0 pass
-all tests currently defined in ``liquid/golden/``.
-
-- Added support for comma separated ``when`` expressions. See #31.
-- The built-in ``join``, ``concat``, ``where``, ``uniq`` and ``compact`` filters now use
-  the new ``sequence_filter`` decorator. ``sequence_filter`` coerces filter left values
-  to array-like objects. ``sequence_filter`` will also flatten nested array-like
-  objects, just like the reference implementation.
-- The built-in ``first``, ``last`` and ``map`` filters now operate on any array-like
-  objects. Previously they were limited to lists and tuples. Strings still don't work.
-- The built-in ``uniq`` and ``compact`` filters now accept an optional argument. If an
-  argument is provided, it should be the name of a property and the left value should be
-  a sequence of objects.
-- The ``size`` filter now returns a default of ``0`` if its left value does not have a 
-  ``__len__`` method.
-- The ``replace`` and ``replace_first`` filters now treat undefined arguments as an
-  empty string.
-- The ``slice`` filter now works on lists, tuples and ranges, as well as strings.
-- Fixed a bug where the ``math_filter`` decorator would cast strings representations of
-  negative integers to a float rather than an int.
-- Added golden test cases for all filters.
-
-
-Version 0.10.2
---------------
-
-- Moved and organized "render" test cases into importable "golden" test cases.
-- Change ``RangeLiteral`` string representation to match the reference implementation.
-- Add newlines to ``tablerow`` output. As per the reference implementation.
-
-Version 0.10.1
---------------
-
-- Range literals can now be assigned, compared and passed as arguments to ``include`` or
-  ``render`` tags. They can also be filtered as if they were an array.
-- Range literals will accept a float value for start and/or stop values. If a float is
-  given for start or stop, it will be cast to an integer.
-- Fixed a bug where the token stream would return the wrong token when peeking
-  immediately after a push.
-
-Version 0.10.0
---------------
-
-- Changed named counter (``increment`` and ``decrement``) scoping. Unless a named
-  counter is shadowed by an ``assign`` or ``capture``, the counter will be in scope for
-  all subsequent Liquid expressions.
-- Changed ``{% increment %}`` to be a post-increment operation. ``{% decrement %}``
-  remains a pre-decrement operation.
-- Added ``forloop.parentloop``. Access parent ``forloop`` objects from nested loops.
-
-Version 0.9.1
--------------
-
-- ``unless`` blocks can now contain ``else`` and ``elsif`` blocks.
-- Added support for array item access with negative indices. Closes #27.
-- Improved error messages for context lookups that resulting an ``IndexError`` or
-  ``KeyError``.
-
-Version 0.9.0
--------------
-
-- Fixed a bug where arguments to ``Template()`` where not being passed to the implicit
-  environment properly (again).
-- Fixed a bug where some errors from the ``sort`` and ``map`` filters were being
-  ignored. Those filters can now raise a ``FilterError``.
-- Removed depreciated class-based filters.
-
-Version 0.8.1
--------------
-
-- Removed ``@abstractmethod`` from ``liquid.loaders.BaseLoader.get_source``. Custom
-  loaders are now free to implement either ``get_source`` or ``get_source_async`` or
-  both. The ``BaseLoader`` implementation of ``get_source`` simply raises a 
-  ``NotImplementedError``.
-- ``liquid.loaders.TemplateSource.uptodate`` (as returned by ``get_source`` and
-  ``get_source_async``) can now be a coroutine function. This means async loaders can
-  check a template's source for changes asynchronously.
-- Added the ``cache_size`` argument to ``Environment`` and ``Template`` for controlling 
-  the capacity of the default template cache.
-- Easier subclassing of ``liquid.parser.ExpressionParser`` with ``END_EXPRESSION``.
-
-Version 0.8.0
--------------
-
-Version bump. Last release before removing depreciated class-based filters.
-
-Version 0.7.8
--------------
-
-- The ``default`` filter now uses ``__liquid__``, if available, when testing an object
-  for truthy-ness.
-
-Version 0.7.7
--------------
-
-- Recursive use of the "render" tag now raises a ``ContextDepthError`` if
-  ``MAX_CONTEXT_DEPTH`` is exceeded. This is now consistent with recursive "include".
-- Drops (custom classes in a render context) can now mimic primitive Liquid values when
-  used as array indexes or hash keys, or in conditional expressions (including 
-  ``case``/``when`` and ``unless``). If defined, the result of calling a drop's
-  ``__liquid__`` method will be used in those scenarios.
-- Added ``base64_encode``, ``base64_decode``, ``base64_url_safe_encode`` and 
-  ``base64_url_safe_decode`` filters.
-- Added asynchronous template loading and rendering. When ``Template.render_async`` is
-  awaited, ``render`` and ``include`` tags will load templates asynchronously. Custom 
-  loaders should implement ``get_source_async``.
-- Added support for asynchronous drops. If a class implements ``__getitem_async__``,
-  which is assumed to be an async version of ``__getitem__``, it will be awaited instead
-  of calling ``__getitem__``.
-
-Version 0.7.6
--------------
-
-- Class-based filters are now depreciated in favour of decorated filter functions. 
-  Abstract filter classes (such as ``liquid.builtin.filters.string.StringFilter``) will
-  be removed in Liquid 0.9.
-- All built-in filters are now implemented as decorated functions. Legacy, class-based
-  filters are no longer registered automatically and will be removed in Liquid 0.9.
-- Legacy filter "helpers" are now depreciated and will be removed in Liquid 0.9. Use
-  the new decorators, like ``liquid.filter.string_filter`` and
-  ``liquid.filter.with_context``, instead.
-- The ``block`` argument to the ``liquid.ast.ConditionalBlockNode`` constructor is no 
-  longer optional.
-
-
-Version 0.7.5
--------------
-
-- Auto reload. Disable automatic reloading of templates by passing ``auto_reload=False``
-  to ``Environment`` or ``Template``. For deployments where template sources don't
-  change between service reloads, setting auto_reload to `False` can yield an increase
-  in performance by avoiding calls to ``uptodate``.
-- Fixed a bug where, when using the ``Template`` API, ``undefined``, ``strict_filters``,
-  and ``autoescape`` where not always passed through to the implicit environment
-  correctly.
-- Added support for continuing a for loop using ``offset: continue``. See #14.
-
-Version 0.7.4
--------------
-
-.. _MarkupSafe: https://github.com/pallets/markupsafe
-
-- HTML auto-escaping. If `MarkupSafe`_ is installed and the ``autoescape`` argument to
-  ``Environment`` or ``Template`` is ``True``, context variables will be HTML escaped
-  automatically.
-- ``LiquidSyntaxError`` exceptions now include the offending template source in the
-  ``source`` property.
-
-Version 0.7.3
--------------
-
-- The built-in ``FileSystemLoader`` now accepts a list of paths to search, in order,
-  returning the first template source found. The ``search_path`` argument can be a
-  string or path-like, or an iterable of strings or path-like objects.
-- Added the ``encoding`` argument to ``FileSystemLoader``. Files will be opened with the
-  given encoding. Defaults to ``"utf-8"``.
-- ``FileSystemLoader`` will raise a ``TemplateNotFound`` exception if ``..`` appears in
-  a template name.
-
-Version 0.7.2
--------------
-
-- Undefined variables are now represented by the ``Undefined`` type, or a subclass of 
-  ``Undefined``. ``Undefined`` behaves like ``nil``, but can also be iterated over and
-  indexed without error.
-- Attempting to loop over an undefined variable no longer raises a ``LiquidTypeError``.
-- Optionally pass ``liquid.StrictUndefined`` as the ``undefined`` argument to
-  ``Template()`` or ``Environment()`` to render in `strict variables` mode. All
-  operations on an instance of ``StrictUndefined`` raise an ``UndefinedError``.
-- Filters can now raise ``FilterValueError`` in addition to ``FilterArgumentError``.
-  Where a ``FilterValueError`` refers to an issue with the left value a filter is
-  applied to.
-- Applying a built-in filter to an undefined variable no longer raises a
-  ``FilterArgumentError`` in most cases.
-- Added the ``strict_filters`` argument to the ``Environment`` and ``Template``
-  constructors. When ``True``, the default, undefined filters raise a
-  ``NoSuchFilterFunc`` exception at render time. When ``False``, undefined filters are
-  silently ignored.
-- The ``join`` filter now forces items in its left value to strings before joining them.
-- The ``join`` filter's argument is now optional, defaulting to a string containing a 
-  single space.
-
-
-Version 0.7.0
--------------
-
-- New ``Template`` API. Create templates from strings without an ``Environment``.
-- The ``template`` object is no longer included in every render context automatically.
-
-Version 0.6.4
--------------
-
-- Implemented the ``blank`` keyword. Empty string and strings containing only whitespace
-  are equal to ``blank``.
-- Implemented the ``null`` keyword. ``null`` is an alias for ``nil``.
-- Implemented the ``ifchanged`` tag.
-
-Version 0.6.2
--------------
-
-- Refactored the standard expression parser. It's now possible to extend 
-  ``ExpressionParser`` for use with custom tags.
-- Decoupled boolean expression parsing for easier ``if``, ``unless`` and ``case`` tag
-  subclassing.
-
-Version 0.6.1
--------------
-
-- Added support for Python 3.7 and PyPy3.7.
-
-
-Version 0.6.0
--------------
-
-- Added support for named filter parameters.
-- The ``default`` filter now accepts the named parameter ``allow_false``.
-- The ``truncate_words`` filter now forces a minimum number of words to 1.
-- The ``newline_to_br`` filter now replaces ``\n`` and ``\r\n`` with ``<br />\n``.
-- The ``strip_newlines`` filter strips ``\r\n``.
diff --git a/LICENSE b/LICENSE
index d9c2697c..69caebb9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2020 James Prior
+Copyright (c) 2020 - 2023 James Prior
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 43529fff..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,4 +0,0 @@
-include README.rst CHANGES.rst LICENSE Pipfile Pipfile.lock liquid/py.typed
-include liquid/resources/*
-graft tests
-global-exclude *.py[cod]
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 19f173f5..00000000
--- a/Makefile
+++ /dev/null
@@ -1,38 +0,0 @@
-
-.PHONY: test
-test:
-	python -m unittest -f
-
-
-.PHONY: coverage
-coverage:
-	python -m coverage erase
-	python -m tox -c tox_cov.ini
-	python -m coverage combine
-	python -m coverage html
-
-.PHONY: benchmark
-benchmark:
-	python -O performance.py
-
-.PHONY: profile
-profile:
-	python -O performance.py --profile
-
-.PHONY: build
-build: clean
-	python -m build
-
-.PHONY: clean
-clean:
-	python -Bc "import pathlib; [p.unlink() for p in pathlib.Path('.').rglob('*.py[co]')]"
-	python -Bc "import pathlib; [p.rmdir() for p in pathlib.Path('.').rglob('__pycache__')]"
-	rm -f .coverage
-	rm -rf .tox
-	rm -rf htmlcov
-	rm -rf python_liquid.egg-info
-	rm -rf build
-	rm -rf dist
-	rm -rf .mypy_cache
-	rm -rf .pytest_cache
-
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 7e547ac3..00000000
--- a/Pipfile
+++ /dev/null
@@ -1,36 +0,0 @@
-[[source]]
-name = "pypi"
-url = "https://pypi.org/simple"
-verify_ssl = true
-
-[dev-packages]
-black = "*"
-pylint = "*"
-flake8 = "*"
-coverage = "*"
-rope = "*"
-pyyaml = "*"
-rstcheck = "*"
-docutils = "*"
-flake8-bugbear = "*"
-twine = "*"
-sphinx = "*"
-mypy = "*"
-mock = "*"
-pytest = "*"
-types-python-dateutil = "*"
-ipython = "*"
-tox = "*"
-build = "*"
-
-[packages]
-python-dateutil = "*"
-liquid = {editable = true, path = "."}
-markupsafe = "*"
-typing-extensions = "*"
-
-[requires]
-python_version = "3.10"
-
-[pipenv]
-allow_prereleases = true
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 751918d0..00000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,1425 +0,0 @@
-{
-    "_meta": {
-        "hash": {
-            "sha256": "672d97e9f67985e1568b2f589caa2d2b2943f6cd19452614daefc108c1253703"
-        },
-        "pipfile-spec": 6,
-        "requires": {
-            "python_version": "3.10"
-        },
-        "sources": [
-            {
-                "name": "pypi",
-                "url": "https://pypi.org/simple",
-                "verify_ssl": true
-            }
-        ]
-    },
-    "default": {
-        "markupsafe": {
-            "hashes": [
-                "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed",
-                "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc",
-                "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2",
-                "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460",
-                "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7",
-                "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0",
-                "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1",
-                "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa",
-                "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03",
-                "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323",
-                "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65",
-                "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013",
-                "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036",
-                "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f",
-                "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4",
-                "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419",
-                "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2",
-                "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619",
-                "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a",
-                "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a",
-                "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd",
-                "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7",
-                "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666",
-                "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65",
-                "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859",
-                "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625",
-                "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff",
-                "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156",
-                "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd",
-                "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba",
-                "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f",
-                "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1",
-                "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094",
-                "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a",
-                "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513",
-                "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed",
-                "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d",
-                "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3",
-                "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147",
-                "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c",
-                "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603",
-                "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601",
-                "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a",
-                "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1",
-                "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d",
-                "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3",
-                "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54",
-                "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2",
-                "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6",
-                "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"
-            ],
-            "index": "pypi",
-            "version": "==2.1.2"
-        },
-        "python-dateutil": {
-            "hashes": [
-                "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
-                "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
-            ],
-            "index": "pypi",
-            "version": "==2.8.2"
-        },
-        "python-liquid": {
-            "editable": true,
-            "path": "."
-        },
-        "six": {
-            "hashes": [
-                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
-                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.16.0"
-        },
-        "typing-extensions": {
-            "hashes": [
-                "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa",
-                "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"
-            ],
-            "index": "pypi",
-            "version": "==4.4.0"
-        }
-    },
-    "develop": {
-        "alabaster": {
-            "hashes": [
-                "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3",
-                "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.7.13"
-        },
-        "astroid": {
-            "hashes": [
-                "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b",
-                "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"
-            ],
-            "markers": "python_full_version >= '3.6.2'",
-            "version": "==2.11.7"
-        },
-        "asttokens": {
-            "hashes": [
-                "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3",
-                "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"
-            ],
-            "version": "==2.2.1"
-        },
-        "attrs": {
-            "hashes": [
-                "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836",
-                "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==22.2.0"
-        },
-        "babel": {
-            "hashes": [
-                "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe",
-                "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2.11.0"
-        },
-        "backcall": {
-            "hashes": [
-                "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e",
-                "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"
-            ],
-            "version": "==0.2.0"
-        },
-        "black": {
-            "hashes": [
-                "sha256:0b945a5a1e5a5321f884de0061d5a8585d947c9b608e37b6d26ceee4dfdf4b62",
-                "sha256:4db1d8027ce7ae53f0ccf02b0be0b8808fefb291d6cb1543420f4165d96d364c",
-                "sha256:5fb7641d442ede92538bc70fa0201f884753a7d0f62f26c722b7b00301b95902",
-                "sha256:63330069d8ec909cf4e2c4d43a7f00aeb03335430ef9fec6cd2328e6ebde8a77",
-                "sha256:793c9176beb2adf295f6b863d9a4dc953fe2ac359ca3da108d71d14cb2c09e52",
-                "sha256:85dede655442f5e246e7abd667fe07e14916897ba52f3640b5489bf11f7dbf67",
-                "sha256:88288a645402106b8eb9f50d7340ae741e16240bb01c2eed8466549153daa96e",
-                "sha256:88ec25a64063945b4591b6378bead544c5d3260de1c93ad96f3ad2d76ddd76fd",
-                "sha256:8dff6f0157e47fbbeada046fca144b6557d3be2fb2602d668881cd179f04a352",
-                "sha256:ca658b69260a18bf7aa0b0a6562dbbd304a737487d1318998aaca5a75901fd2c",
-                "sha256:ddbf9da228726d46f45c29024263e160d41030a415097254817d65127012d1a2",
-                "sha256:e88e4b633d64b9e7adc4a6b922f52bb204af9f90d7b1e3317e6490f2b598b1ea"
-            ],
-            "index": "pypi",
-            "version": "==23.1a1"
-        },
-        "bleach": {
-            "hashes": [
-                "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a",
-                "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==5.0.1"
-        },
-        "build": {
-            "hashes": [
-                "sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171",
-                "sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"
-            ],
-            "index": "pypi",
-            "version": "==0.10.0"
-        },
-        "cachetools": {
-            "hashes": [
-                "sha256:5991bc0e08a1319bb618d3195ca5b6bc76646a49c21d55962977197b301cc1fe",
-                "sha256:8462eebf3a6c15d25430a8c27c56ac61340b2ecf60c9ce57afc2b97e450e47da"
-            ],
-            "markers": "python_version ~= '3.7'",
-            "version": "==5.2.1"
-        },
-        "certifi": {
-            "hashes": [
-                "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
-                "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2022.12.7"
-        },
-        "cffi": {
-            "hashes": [
-                "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
-                "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
-                "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
-                "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
-                "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
-                "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
-                "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
-                "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
-                "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
-                "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
-                "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
-                "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
-                "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
-                "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
-                "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
-                "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
-                "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
-                "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
-                "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
-                "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
-                "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
-                "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
-                "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
-                "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
-                "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
-                "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
-                "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
-                "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
-                "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
-                "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
-                "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
-                "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
-                "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
-                "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
-                "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
-                "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
-                "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
-                "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
-                "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
-                "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
-                "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
-                "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
-                "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
-                "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
-                "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
-                "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
-                "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
-                "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
-                "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
-                "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
-                "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
-                "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
-                "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
-                "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
-                "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
-                "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
-                "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
-                "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
-                "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
-                "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
-                "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
-                "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
-                "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
-                "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
-            ],
-            "version": "==1.15.1"
-        },
-        "chardet": {
-            "hashes": [
-                "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5",
-                "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==5.1.0"
-        },
-        "charset-normalizer": {
-            "hashes": [
-                "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b",
-                "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42",
-                "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d",
-                "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b",
-                "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a",
-                "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59",
-                "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154",
-                "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1",
-                "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c",
-                "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a",
-                "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d",
-                "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6",
-                "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b",
-                "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b",
-                "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783",
-                "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5",
-                "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918",
-                "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555",
-                "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639",
-                "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786",
-                "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e",
-                "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed",
-                "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820",
-                "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8",
-                "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3",
-                "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541",
-                "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14",
-                "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be",
-                "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e",
-                "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76",
-                "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b",
-                "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c",
-                "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b",
-                "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3",
-                "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc",
-                "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6",
-                "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59",
-                "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4",
-                "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d",
-                "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d",
-                "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3",
-                "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a",
-                "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea",
-                "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6",
-                "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e",
-                "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603",
-                "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24",
-                "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a",
-                "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58",
-                "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678",
-                "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a",
-                "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c",
-                "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6",
-                "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18",
-                "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174",
-                "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317",
-                "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f",
-                "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc",
-                "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837",
-                "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41",
-                "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c",
-                "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579",
-                "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753",
-                "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8",
-                "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291",
-                "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087",
-                "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866",
-                "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3",
-                "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d",
-                "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1",
-                "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca",
-                "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e",
-                "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db",
-                "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72",
-                "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d",
-                "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc",
-                "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539",
-                "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d",
-                "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af",
-                "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b",
-                "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602",
-                "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f",
-                "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478",
-                "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c",
-                "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e",
-                "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479",
-                "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7",
-                "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"
-            ],
-            "markers": "python_full_version >= '3.6.0'",
-            "version": "==3.0.1"
-        },
-        "click": {
-            "hashes": [
-                "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
-                "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==8.1.3"
-        },
-        "colorama": {
-            "hashes": [
-                "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
-                "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
-            "version": "==0.4.6"
-        },
-        "commonmark": {
-            "hashes": [
-                "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60",
-                "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"
-            ],
-            "version": "==0.9.1"
-        },
-        "coverage": {
-            "hashes": [
-                "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45",
-                "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809",
-                "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4",
-                "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b",
-                "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7",
-                "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0",
-                "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0",
-                "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea",
-                "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2",
-                "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a",
-                "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45",
-                "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b",
-                "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209",
-                "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca",
-                "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab",
-                "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095",
-                "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7",
-                "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6",
-                "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af",
-                "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499",
-                "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831",
-                "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637",
-                "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2",
-                "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb",
-                "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029",
-                "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc",
-                "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8",
-                "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f",
-                "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2",
-                "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d",
-                "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289",
-                "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c",
-                "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded",
-                "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96",
-                "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0",
-                "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904",
-                "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21",
-                "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89",
-                "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78",
-                "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad",
-                "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196",
-                "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd",
-                "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0",
-                "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882",
-                "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757",
-                "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16",
-                "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0",
-                "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47",
-                "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40",
-                "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1",
-                "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"
-            ],
-            "index": "pypi",
-            "version": "==7.0.5"
-        },
-        "cryptography": {
-            "hashes": [
-                "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b",
-                "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f",
-                "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190",
-                "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f",
-                "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f",
-                "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb",
-                "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c",
-                "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773",
-                "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72",
-                "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8",
-                "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717",
-                "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9",
-                "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856",
-                "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96",
-                "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288",
-                "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39",
-                "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e",
-                "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce",
-                "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1",
-                "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de",
-                "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df",
-                "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf",
-                "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==39.0.0"
-        },
-        "decorator": {
-            "hashes": [
-                "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330",
-                "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==5.1.1"
-        },
-        "dill": {
-            "hashes": [
-                "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0",
-                "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==0.3.6"
-        },
-        "distlib": {
-            "hashes": [
-                "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46",
-                "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"
-            ],
-            "version": "==0.3.6"
-        },
-        "docutils": {
-            "hashes": [
-                "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6",
-                "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"
-            ],
-            "index": "pypi",
-            "version": "==0.19"
-        },
-        "exceptiongroup": {
-            "hashes": [
-                "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e",
-                "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"
-            ],
-            "markers": "python_version < '3.11'",
-            "version": "==1.1.0"
-        },
-        "executing": {
-            "hashes": [
-                "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc",
-                "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"
-            ],
-            "version": "==1.2.0"
-        },
-        "filelock": {
-            "hashes": [
-                "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de",
-                "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==3.9.0"
-        },
-        "flake8": {
-            "hashes": [
-                "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7",
-                "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"
-            ],
-            "index": "pypi",
-            "version": "==6.0.0"
-        },
-        "flake8-bugbear": {
-            "hashes": [
-                "sha256:4be3722cb79385684ffe375e4986495ce28dd0e1daa7affaddf3b17abdbc375b",
-                "sha256:f26788e4573c9f2ba89c787941da77e80f7818172cc5de042029924d4c07a021"
-            ],
-            "index": "pypi",
-            "version": "==23.1.17"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
-                "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==3.4"
-        },
-        "imagesize": {
-            "hashes": [
-                "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b",
-                "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.4.1"
-        },
-        "importlib-metadata": {
-            "hashes": [
-                "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad",
-                "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==6.0.0"
-        },
-        "iniconfig": {
-            "hashes": [
-                "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
-                "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.0.0"
-        },
-        "ipython": {
-            "hashes": [
-                "sha256:da01e6df1501e6e7c32b5084212ddadd4ee2471602e2cf3e0190f4de6b0ea481",
-                "sha256:f3bf2c08505ad2c3f4ed5c46ae0331a8547d36bf4b21a451e8ae80c0791db95b"
-            ],
-            "index": "pypi",
-            "version": "==8.8.0"
-        },
-        "isort": {
-            "hashes": [
-                "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6",
-                "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"
-            ],
-            "markers": "python_full_version >= '3.7.0'",
-            "version": "==5.11.4"
-        },
-        "jaraco.classes": {
-            "hashes": [
-                "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158",
-                "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==3.2.3"
-        },
-        "jedi": {
-            "hashes": [
-                "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e",
-                "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.18.2"
-        },
-        "jeepney": {
-            "hashes": [
-                "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806",
-                "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"
-            ],
-            "markers": "sys_platform == 'linux'",
-            "version": "==0.8.0"
-        },
-        "jinja2": {
-            "hashes": [
-                "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
-                "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==3.1.2"
-        },
-        "keyring": {
-            "hashes": [
-                "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd",
-                "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==23.13.1"
-        },
-        "lazy-object-proxy": {
-            "hashes": [
-                "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382",
-                "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82",
-                "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9",
-                "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494",
-                "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46",
-                "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30",
-                "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63",
-                "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4",
-                "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae",
-                "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be",
-                "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701",
-                "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd",
-                "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006",
-                "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a",
-                "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586",
-                "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8",
-                "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821",
-                "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07",
-                "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b",
-                "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171",
-                "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b",
-                "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2",
-                "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7",
-                "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4",
-                "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8",
-                "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e",
-                "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f",
-                "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda",
-                "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4",
-                "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e",
-                "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671",
-                "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11",
-                "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455",
-                "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734",
-                "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb",
-                "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==1.9.0"
-        },
-        "markupsafe": {
-            "hashes": [
-                "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed",
-                "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc",
-                "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2",
-                "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460",
-                "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7",
-                "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0",
-                "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1",
-                "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa",
-                "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03",
-                "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323",
-                "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65",
-                "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013",
-                "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036",
-                "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f",
-                "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4",
-                "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419",
-                "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2",
-                "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619",
-                "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a",
-                "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a",
-                "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd",
-                "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7",
-                "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666",
-                "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65",
-                "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859",
-                "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625",
-                "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff",
-                "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156",
-                "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd",
-                "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba",
-                "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f",
-                "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1",
-                "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094",
-                "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a",
-                "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513",
-                "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed",
-                "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d",
-                "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3",
-                "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147",
-                "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c",
-                "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603",
-                "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601",
-                "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a",
-                "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1",
-                "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d",
-                "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3",
-                "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54",
-                "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2",
-                "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6",
-                "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"
-            ],
-            "index": "pypi",
-            "version": "==2.1.2"
-        },
-        "matplotlib-inline": {
-            "hashes": [
-                "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311",
-                "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==0.1.6"
-        },
-        "mccabe": {
-            "hashes": [
-                "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
-                "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.7.0"
-        },
-        "mock": {
-            "hashes": [
-                "sha256:c41cfb1e99ba5d341fbcc5308836e7d7c9786d302f995b2c271ce2144dece9eb",
-                "sha256:e3ea505c03babf7977fd21674a69ad328053d414f05e6433c30d8fa14a534a6b"
-            ],
-            "index": "pypi",
-            "version": "==5.0.1"
-        },
-        "more-itertools": {
-            "hashes": [
-                "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41",
-                "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==9.0.0"
-        },
-        "mypy": {
-            "hashes": [
-                "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d",
-                "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6",
-                "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf",
-                "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f",
-                "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813",
-                "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33",
-                "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad",
-                "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05",
-                "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297",
-                "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06",
-                "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd",
-                "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243",
-                "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305",
-                "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476",
-                "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711",
-                "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70",
-                "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5",
-                "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461",
-                "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab",
-                "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c",
-                "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d",
-                "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135",
-                "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93",
-                "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648",
-                "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a",
-                "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb",
-                "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3",
-                "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372",
-                "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb",
-                "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"
-            ],
-            "index": "pypi",
-            "version": "==0.991"
-        },
-        "mypy-extensions": {
-            "hashes": [
-                "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
-                "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
-            ],
-            "version": "==0.4.3"
-        },
-        "packaging": {
-            "hashes": [
-                "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
-                "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==23.0"
-        },
-        "parso": {
-            "hashes": [
-                "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0",
-                "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.8.3"
-        },
-        "pathspec": {
-            "hashes": [
-                "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6",
-                "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==0.10.3"
-        },
-        "pexpect": {
-            "hashes": [
-                "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937",
-                "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"
-            ],
-            "markers": "sys_platform != 'win32'",
-            "version": "==4.8.0"
-        },
-        "pickleshare": {
-            "hashes": [
-                "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
-                "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
-            ],
-            "version": "==0.7.5"
-        },
-        "pkginfo": {
-            "hashes": [
-                "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546",
-                "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==1.9.6"
-        },
-        "platformdirs": {
-            "hashes": [
-                "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490",
-                "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.6.2"
-        },
-        "pluggy": {
-            "hashes": [
-                "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
-                "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==1.0.0"
-        },
-        "prompt-toolkit": {
-            "hashes": [
-                "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63",
-                "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"
-            ],
-            "markers": "python_full_version >= '3.6.2'",
-            "version": "==3.0.36"
-        },
-        "ptyprocess": {
-            "hashes": [
-                "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35",
-                "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"
-            ],
-            "version": "==0.7.0"
-        },
-        "pure-eval": {
-            "hashes": [
-                "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350",
-                "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"
-            ],
-            "version": "==0.2.2"
-        },
-        "pycodestyle": {
-            "hashes": [
-                "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
-                "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2.10.0"
-        },
-        "pycparser": {
-            "hashes": [
-                "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
-                "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
-            ],
-            "version": "==2.21"
-        },
-        "pydantic": {
-            "hashes": [
-                "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72",
-                "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423",
-                "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f",
-                "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c",
-                "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06",
-                "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53",
-                "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774",
-                "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6",
-                "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c",
-                "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f",
-                "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6",
-                "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3",
-                "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817",
-                "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903",
-                "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a",
-                "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e",
-                "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d",
-                "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85",
-                "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00",
-                "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28",
-                "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3",
-                "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024",
-                "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4",
-                "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e",
-                "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d",
-                "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa",
-                "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854",
-                "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15",
-                "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648",
-                "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8",
-                "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c",
-                "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857",
-                "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f",
-                "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416",
-                "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978",
-                "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==1.10.4"
-        },
-        "pyflakes": {
-            "hashes": [
-                "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf",
-                "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==3.0.1"
-        },
-        "pygments": {
-            "hashes": [
-                "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297",
-                "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2.14.0"
-        },
-        "pylint": {
-            "hashes": [
-                "sha256:15bc7b37f2022720765247eec24c49b93dcae6c81418adc3c36621b13d946f6d",
-                "sha256:add6bc6380143af0f7a1ab15b5b5f54a8d9d25afc99fc7e2d86fd66f6c3e1ad9"
-            ],
-            "index": "pypi",
-            "version": "==3.0.0a5"
-        },
-        "pyproject-api": {
-            "hashes": [
-                "sha256:0962df21f3e633b8ddb9567c011e6c1b3dcdfc31b7860c0ede7e24c5a1200fbe",
-                "sha256:4c111277dfb96bcd562c6245428f27250b794bfe3e210b8714c4f893952f2c17"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==1.5.0"
-        },
-        "pyproject-hooks": {
-            "hashes": [
-                "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8",
-                "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==1.0.0"
-        },
-        "pytest": {
-            "hashes": [
-                "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5",
-                "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"
-            ],
-            "index": "pypi",
-            "version": "==7.2.1"
-        },
-        "pytoolconfig": {
-            "extras": [
-                "global"
-            ],
-            "hashes": [
-                "sha256:5e1a246f77970ddb5f3d8d06fbf162424b8a1adfc22c2eb51826b383bf293d1e",
-                "sha256:7befe396f91b2593345b829a7745c7f459f04fc6c53fc86c0b771945446a7bd1"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==1.2.4"
-        },
-        "pytz": {
-            "hashes": [
-                "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0",
-                "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"
-            ],
-            "version": "==2022.7.1"
-        },
-        "pyyaml": {
-            "hashes": [
-                "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf",
-                "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
-                "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
-                "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
-                "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b",
-                "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4",
-                "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07",
-                "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba",
-                "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9",
-                "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
-                "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
-                "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
-                "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782",
-                "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
-                "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
-                "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
-                "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
-                "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
-                "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1",
-                "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
-                "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
-                "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
-                "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
-                "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
-                "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
-                "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d",
-                "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
-                "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
-                "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7",
-                "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
-                "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
-                "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
-                "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358",
-                "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
-                "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
-                "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
-                "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
-                "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f",
-                "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
-                "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
-            ],
-            "index": "pypi",
-            "version": "==6.0"
-        },
-        "readme-renderer": {
-            "hashes": [
-                "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273",
-                "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==37.3"
-        },
-        "requests": {
-            "hashes": [
-                "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa",
-                "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"
-            ],
-            "markers": "python_version >= '3.7' and python_version < '4'",
-            "version": "==2.28.2"
-        },
-        "requests-toolbelt": {
-            "hashes": [
-                "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7",
-                "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==0.10.1"
-        },
-        "rfc3986": {
-            "hashes": [
-                "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd",
-                "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.0.0"
-        },
-        "rich": {
-            "hashes": [
-                "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e",
-                "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"
-            ],
-            "markers": "python_full_version >= '3.6.3' and python_full_version < '4.0.0'",
-            "version": "==12.6.0"
-        },
-        "rope": {
-            "hashes": [
-                "sha256:893dd80ba7077fc9f6f42b0a849372076b70f1d6e405b9f0cc52781ffa0e6890",
-                "sha256:ba39581d0f8dee4ae8b5b5e82e35d03cebad965ccb127b7eaab9755cdc85e85a"
-            ],
-            "index": "pypi",
-            "version": "==1.7.0"
-        },
-        "rstcheck": {
-            "hashes": [
-                "sha256:8e43485a644e794b8127f8c4868ef62c14ec7919bdda6cb16642157055d32e47",
-                "sha256:edeff9ad0644d12bd250100b677887424193789254c90d95c13375062ee2cbac"
-            ],
-            "index": "pypi",
-            "version": "==6.1.1"
-        },
-        "rstcheck-core": {
-            "hashes": [
-                "sha256:add19c9a1b97d9087f4b463b49c12cd8a9c03689a255e99089c70a2692f16369",
-                "sha256:d75d7df8f15b58e8aafe322d6fb6ef1ac8d12bb563089b0696948a00ee7f601a"
-            ],
-            "markers": "python_version >= '3.7' and python_version < '4.0'",
-            "version": "==1.0.3"
-        },
-        "secretstorage": {
-            "hashes": [
-                "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77",
-                "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"
-            ],
-            "markers": "sys_platform == 'linux'",
-            "version": "==3.3.3"
-        },
-        "setuptools": {
-            "hashes": [
-                "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab",
-                "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==66.0.0"
-        },
-        "shellingham": {
-            "hashes": [
-                "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744",
-                "sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28"
-            ],
-            "version": "==1.5.0.post1"
-        },
-        "six": {
-            "hashes": [
-                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
-                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.16.0"
-        },
-        "snowballstemmer": {
-            "hashes": [
-                "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1",
-                "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"
-            ],
-            "version": "==2.2.0"
-        },
-        "sphinx": {
-            "hashes": [
-                "sha256:0dac3b698538ffef41716cf97ba26c1c7788dba73ce6f150c1ff5b4720786dd2",
-                "sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc"
-            ],
-            "index": "pypi",
-            "version": "==6.1.3"
-        },
-        "sphinxcontrib-applehelp": {
-            "hashes": [
-                "sha256:83749f09f6ac843b8cb685277dbc818a8bf2d76cc19602699094fe9a74db529e",
-                "sha256:ba0f2a22e6eeada8da6428d0d520215ee8864253f32facf958cca81e426f661d"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==1.0.3"
-        },
-        "sphinxcontrib-devhelp": {
-            "hashes": [
-                "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
-                "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.2"
-        },
-        "sphinxcontrib-htmlhelp": {
-            "hashes": [
-                "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07",
-                "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2.0.0"
-        },
-        "sphinxcontrib-jsmath": {
-            "hashes": [
-                "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
-                "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.1"
-        },
-        "sphinxcontrib-qthelp": {
-            "hashes": [
-                "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
-                "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.3"
-        },
-        "sphinxcontrib-serializinghtml": {
-            "hashes": [
-                "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd",
-                "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.1.5"
-        },
-        "stack-data": {
-            "hashes": [
-                "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815",
-                "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"
-            ],
-            "version": "==0.6.2"
-        },
-        "tomli": {
-            "hashes": [
-                "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
-                "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
-            ],
-            "markers": "python_version < '3.11'",
-            "version": "==2.0.1"
-        },
-        "tomlkit": {
-            "hashes": [
-                "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b",
-                "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.11.6"
-        },
-        "tox": {
-            "hashes": [
-                "sha256:307993257d792a12a63ff86a0b67a71a5ab2d4a2cc12bbae947115224d4ac3fb",
-                "sha256:6dddc4c69718b65aacbef0921ffa8af065b0f63ff0b24ffc403477975853c918"
-            ],
-            "index": "pypi",
-            "version": "==4.3.5"
-        },
-        "traitlets": {
-            "hashes": [
-                "sha256:32500888f5ff7bbf3b9267ea31748fa657aaf34d56d85e60f91dda7dc7f5785b",
-                "sha256:a1ca5df6414f8b5760f7c5f256e326ee21b581742114545b462b35ffe3f04861"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==5.8.1"
-        },
-        "twine": {
-            "hashes": [
-                "sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8",
-                "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8"
-            ],
-            "index": "pypi",
-            "version": "==4.0.2"
-        },
-        "typer": {
-            "extras": [
-                "all"
-            ],
-            "hashes": [
-                "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d",
-                "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.7.0"
-        },
-        "types-docutils": {
-            "hashes": [
-                "sha256:7be44dac1b160901308ac43762d5e89175105ed682092a3a1f08911d2573a470",
-                "sha256:ca3d2135484adb52dd042bbddbd6eddcbbda8c608ba3f5e5f908bd548ffcb399"
-            ],
-            "version": "==0.19.1.2"
-        },
-        "types-python-dateutil": {
-            "hashes": [
-                "sha256:4a6f4cc19ce4ba1a08670871e297bf3802f55d4f129e6aa2443f540b6cf803d2",
-                "sha256:cfb7d31021c6bce6f3362c69af6e3abb48fe3e08854f02487e844ff910deec2a"
-            ],
-            "index": "pypi",
-            "version": "==2.8.19.6"
-        },
-        "typing-extensions": {
-            "hashes": [
-                "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa",
-                "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"
-            ],
-            "index": "pypi",
-            "version": "==4.4.0"
-        },
-        "urllib3": {
-            "hashes": [
-                "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
-                "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
-            "version": "==1.26.14"
-        },
-        "virtualenv": {
-            "hashes": [
-                "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4",
-                "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==20.17.1"
-        },
-        "wcwidth": {
-            "hashes": [
-                "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e",
-                "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"
-            ],
-            "version": "==0.2.6"
-        },
-        "webencodings": {
-            "hashes": [
-                "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
-                "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
-            ],
-            "version": "==0.5.1"
-        },
-        "wrapt": {
-            "hashes": [
-                "sha256:09fbbcfb17be9464c18f3e54457e4907a3913f836a691552ae1c967df4d6e4b4",
-                "sha256:0d47527c5c8f9fa6d20fd72e87c39c1cad627f8c7f11b62af05e27ddcd443ba3",
-                "sha256:107415c7367bf5489ab7b16f5348216a1a1c4ff8d5ec08612cf6f64321f9ad3c",
-                "sha256:123de234ee3f528ae817a4be29f811b9ce25f9f48ed446fbd5adcf68b7abb869",
-                "sha256:1b2975c345967fbbff2fa9ffc45c60c30de005e753b6c01135ba4fbb788d3f38",
-                "sha256:21c1487e21979ba8a6a1d6e2e2d026bfeb57aebec0e25da2f7960938e99a9e8c",
-                "sha256:25be3596f51338a6043c6679870d95eb0636e02bb1e6ccf63da61e8021e533d0",
-                "sha256:26bc4fe48eb78cc6a1328b70fe720555b4d31321b060b69fc03a1f8296ee40de",
-                "sha256:30201cbcfea303e345b3c83fb7217e34c46c87df897d3a6f9e0cb840cc0f81c9",
-                "sha256:32960f2f9e5f53ff8c75ef9704d03114fc14c8f604a75323aa7b907f65bfcb6b",
-                "sha256:338dd5c161be7c981f8afb597dac0f5d26e9310d66bbf612209fa29c21a7da33",
-                "sha256:3a8981cbf86e0a004160cbd299af99cb251f93a49d58032bd7400b23f40839e5",
-                "sha256:3cdfb26e3e9fec79e04af3e37b0576c12ee3cd5aa7ba3f041b92aa323fb7f997",
-                "sha256:401a64a7cdfe4d00edcd6a36666e6fba0f61291f6bacf686baf918182f02cf2d",
-                "sha256:42a271aac91a2c74ea6a6b869219cff14404e9362de05c4b83f65dfed7e9b12c",
-                "sha256:44bd131dab4ed4d6221fa78277b6ccbcfa9b439629ffa930b1d1ddc38a88a224",
-                "sha256:45d9589ff8649934c2106d57b85405777e9ce1278eaf0f0fec70f5e037d82c48",
-                "sha256:485cdea587ccc1e8d483855e4e26ab4397b6455e2b416c1f8034ac0acd820da8",
-                "sha256:49e9cb44795b512097f09d9df29e0d83d5f2a517a00eec47d886c0ded5be4496",
-                "sha256:4c08d4c4698e3119408acfa48871ed210a9c40d566b0426eee0b2bd0bc638c0e",
-                "sha256:4c74bf0a62faed5e40431fa846f278420d2356fe2a781b5f601946749f9ac914",
-                "sha256:5053e18f970b38e2e8cb4f635bba79ef1a22d236795af73017b415e9a9aeba48",
-                "sha256:50bfbb843e37e767634102eaf284def65a381061ca762f536c6b5a40e4aed4f4",
-                "sha256:515ab064a2163a05a3b6460137dc4c28c091a58853ed4a0b48b100089a29cb55",
-                "sha256:57ec643799932381502c3f0db4ef5de25bc10b0adc02b0de5fb2a61f23ca7562",
-                "sha256:5951d10d4cac32454c0a65e83330f832db7c4208eee82a69cd7e9be06bbecb70",
-                "sha256:59caed62ad5f7ce1a917e758e15bd2f0bfe2fd6817d0cf8c5e1a713203481669",
-                "sha256:5ac1a93b98c131aabc145a8712e01ed0241c66cb5940fe69ba34c61d59d8b31a",
-                "sha256:5cc4a8d20edb25b395c62e6932e5a1c8afe1aaf45bf2acd876ba683306f9dfa0",
-                "sha256:5d972d3a48c199564e0659d62de4a39d123a547dc25eba548e97b42e4733c0a8",
-                "sha256:5f3a8c55f27524c0426b07232dbfb7f2e0ba414dcb57ebc66054f0a170a0fc49",
-                "sha256:62324e76ea16e5b0339de237edfc1df338442308c599ad3ca02a22e5b3d847db",
-                "sha256:654160de93ee85253d426d9390a649bf2b91e3f79560410644664fd284bc2be8",
-                "sha256:66938fd0469076f8fb1064b24778756b47d71348e5ca7bde5f3cec303ed90676",
-                "sha256:67cad5aae1d0934b02f6a15fa6f181ef83e86d5d8dee2b26873ff57cdd4c0f4f",
-                "sha256:698e03c02d501681939f8a4376afb5521ec04dcca9bfdc023cc968e457e50410",
-                "sha256:6d03a9c18017976265d07e40236dd09278a387cdf3870487060ecf4f48ea5252",
-                "sha256:72d9f2ff2fb3e5c2bfcf891152995b8589c5e735045fbbaae00bd776a6dc9a6b",
-                "sha256:73942a75f7d8b78630076075848d303be3d68798f0462073639583616f0f65a0",
-                "sha256:77cb6f9eae637b1d19b78738602317f788df1896b79f8d81b829d186ff8154c3",
-                "sha256:7963d8049c452e9462e9ea9bc1a20aa31bda4ce667a39b38013e84ae1e6185b0",
-                "sha256:79662ce899950115f33f657b1558a83f631b2a2f91d6fcfdd09a6cbe378cbd2f",
-                "sha256:798bb2eb07988514ee3d213d8ab35e6677c86215190896970b1c407b84ce791a",
-                "sha256:7afabb46692c9dc3053a526915a05ddbc41266082ee505c64fc5aa1fd8ccfdd9",
-                "sha256:7bf51f7815f15d6655c67d32ceedc97787e88c41d1c9cfab19c5f54982d09c41",
-                "sha256:81cf008c5ef139b63dac5df0035eb597bc0b5818d321219d41da2c1aa2fa8ce3",
-                "sha256:88b8bfb2ce44d91789a566cd2be7083e0520208977322bf84ddd215fbee81b58",
-                "sha256:8ee0105bca55eb430ef949eadea557915a183810f25f396e91b5587b7aff3348",
-                "sha256:8f0a1f9d970d4af52c4b696022268b710e19f446423a59cf1d9c393b98c0d985",
-                "sha256:93d95f9007cdcd2ab1c31761840434aff21bc75859110b28dd77a1016fa39202",
-                "sha256:94f9d8cd233b0654c3c38b0855e872716a5b358203a42d41275acb09f3526f0a",
-                "sha256:99011767b4b383706ac749d52b6e2b1f7335e45a72ba604465aa097cca61da3d",
-                "sha256:9c025bbc3b29ca7d56cdbe8dd88e7c90a4caea59af1a236cc17222b114047342",
-                "sha256:9da305a413ac7be48dbcc2eda0a6376235a9cd437a3eb106e67cca36f50f9c12",
-                "sha256:9f74533bf0490662451e520450111548cd669440bb30f0400abe9e778789f5b9",
-                "sha256:a7f4fbef92c3b507232c65b9504c1f23dd5faede097b55ac5ffa599ae775c6e8",
-                "sha256:b51f96d1cd7a88baa373bbb39433d09326fd204c8bf95c2f53b81c530df688ef",
-                "sha256:b53b375ef6028136856aca7c22126de55343035d60198d32d32a90e156be857a",
-                "sha256:b86269bf2657876413a34c84fe205e853aa28ea84d7d044b6a58fd59f6c84e7c",
-                "sha256:bb6182ff6760da884274150af7b7dd3b1a71b49e7c2daa1398e48929b453022a",
-                "sha256:c47994d3ac60e235614235aef76fae1d41a2f882f5fbd62b6fde92f29d84d785",
-                "sha256:c4e87511f8b06cfdf8555701abe64bd2bd0951d1c2a5e66f9ee652ea38edeb5c",
-                "sha256:c588d8e582f07ae24133a4e12ff10e09789763a55acc2317760507492d0d482e",
-                "sha256:c9239fef921d5503c1b4489861ecabe246f56791c0804b1afb6917ba0f328002",
-                "sha256:d46da7b89f9b09c232528b395e9e27be7a73a2b70745eca2e4580f06fe622be4",
-                "sha256:d79b8d62443414fd8790e5b3f89365161957b40e1275888f8e9390a778553bb9",
-                "sha256:ddf53a1f0184a9d9bd72af31cbd639e48e5affc8cc473246a5fe2f89e7b01741",
-                "sha256:e3855ef2ffebca0fb8a1e80bfde292bcb19cdd43c73b94b2b91c874cca1116b9",
-                "sha256:e494137d27700a3a3b020c903d511f1f9fc7ecec406991266a023a756e61db2e",
-                "sha256:e7e3ba93fdec4e044a98c955641dd4e978513854d4a9425fd19f15143bc6af02",
-                "sha256:e8666f3b83285e84a081fc4260fd044f7ef06843691a3fa5e87432152ceeb7c3",
-                "sha256:e999014967cf06782702c7308c3e7977035f548217743eba752b2f8e0b576ee9",
-                "sha256:f50f3bb2ba9d914830ac351a6fce9fd3a5b8ff28124966941f0ea1a4789339a4",
-                "sha256:f6c7af527e9757af49b0de3f9447a61ad3ebc0cedb8e2ecd900265f715944a16",
-                "sha256:f6d36e191323cef8143915e0acc708fd222179311daec90a677f06270367fe8b"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==1.15.0rc1"
-        },
-        "zipp": {
-            "hashes": [
-                "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa",
-                "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==3.11.0"
-        }
-    }
-}
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..a5b70d3d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,149 @@
+<h1 align="center">Python Liquid</h1>
+
+<p align="center" style="margin-bottom: 5px">
+A Python implementation of <a href="https://shopify.github.io/liquid/">Liquid</a>, the safe customer-facing template language for flexible web apps.
+</p>
+
+<p align="center" style="margin-bottom: 5px">
+  <a href="https://github.com/jg-rp/liquid/blob/main/LICENSE">
+    <img src="https://img.shields.io/pypi/l/python-liquid.svg?style=flat-square" alt="License">
+  </a>
+</p>
+
+<p align="center" style="margin-bottom: 5px">
+  <a href="https://pypi.org/project/python-liquid/">
+    <img src="https://img.shields.io/pypi/v/python-liquid.svg?style=flat-square" alt="PyPi - Version">
+  </a>
+  <a href="https://anaconda.org/conda-forge/python-liquid">
+    <img src="https://img.shields.io/conda/vn/conda-forge/python-liquid?style=flat-square" alt="conda-forge">
+  </a>
+</p>
+
+<p align="center" style="margin-bottom: 5px">
+  <a href="https://pypi.org/project/python-liquid/">
+    <img src="https://img.shields.io/pypi/pyversions/python-liquid.svg?style=flat-square" alt="Python versions">
+  </a>
+  <a href="https://github.com/jg-rp/liquid/actions/workflows/coverage.yaml">
+    <img src="https://img.shields.io/badge/pypy-3.7%20%7C%203.8%20%7C%203.9-blue?style=flat-square" alt="PyPy versions">
+  </a>
+</p>
+
+<p align="center" style="margin-bottom: 5px">
+  <a href="https://github.com/jg-rp/liquid/actions/workflows/tests.yaml">
+    <img src="https://img.shields.io/github/actions/workflow/status/jg-rp/liquid/tests.yaml?branch=main&label=tests&style=flat-square" alt="Tests">
+  </a>
+  <a href="https://github.com/jg-rp/liquid/actions/workflows/coverage.yaml">
+    <img src="https://img.shields.io/github/actions/workflow/status/jg-rp/liquid/coverage.yaml?branch=main&label=coverage&style=flat-square" alt="Coverage">
+  </a>
+</p>
+
+---
+
+**Table of Contents**
+
+- [Install](#install)
+- [Links](#links)
+- [Example](#example)
+- [Related Projects](#related-projects)
+- [Compatibility](#compatibility)
+- [Benchmark](#benchmark)
+- [Contributing](#contributing)
+
+## Install
+
+Install Python Liquid using [Pipenv](https://pipenv.pypa.io/en/latest/):
+
+```shell
+$ pipenv install -u python-liquid
+```
+
+Or [pip](https://pip.pypa.io/en/stable/getting-started/):
+
+```shell
+$ pip install python-liquid
+```
+
+Or from [conda-forge](https://anaconda.org/conda-forge/python-liquid):
+
+```shell
+$ conda install -c conda-forge python-liquid
+```
+
+## Links
+
+- Documentation: https://jg-rp.github.io/liquid/
+- Change Log: https://github.com/jg-rp/liquid/blob/main/CHANGES.md
+- PyPi: https://pypi.org/project/python-liquid/
+- Source Code: https://github.com/jg-rp/liquid
+- Issue Tracker: https://github.com/jg-rp/liquid/issues
+
+## Example
+
+```python
+from liquid import Template
+
+template = Template("Hello, {{ you }}!")
+print(template.render(you="World"))  # "Hello, World!"
+print(template.render(you="Liquid"))  # "Hello, Liquid!"
+```
+
+## Related Projects
+
+- [liquid-babel](https://github.com/jg-rp/liquid-babel) Internationalization and localization for Liquid templates.
+- [LiquidScript](https://github.com/jg-rp/liquidscript): A JavaScript and TypeScript engine for Liquid with a similar high-level API to Python Liquid.
+- [django-liquid](https://github.com/jg-rp/django-liquid): A Django template backend for Liquid. Render Liquid templates in your Django apps.
+- [Flask-Liquid](https://github.com/jg-rp/Flask-Liquid): A Flask extension for Liquid. Render Liquid templates in your Flask applications.
+- [golden-liquid](https://github.com/jg-rp/golden-liquid): A test suite for Liquid. See how various Liquid template engines compare to the reference implementation.
+
+## Compatibility
+
+We strive to be 100% compatible with the [reference implementation](https://shopify.github.io/liquid/) of Liquid, written in Ruby. That is, given an equivalent render context, a template rendered with Python Liquid should produce the same output as when rendered with Ruby Liquid.
+
+See the [known issues page](https://jg-rp.github.io/liquid/known_issues) for details of known incompatibilities between Python Liquid and Ruby Liquid, and please help by raising an issue if you notice an incompatibility.
+
+## Benchmark
+
+You can run the benchmark using `hatch run benchmark` (or `python -O scripts/performance.py` if you don't have `make`) from the root of the source tree. On my ropey desktop computer with a Ryzen 5 1500X and Python 3.11.0, we get the following results.
+
+```text
+Best of 5 rounds with 100 iterations per round and 60 ops per iteration (6000 ops per round).
+
+lex template (not expressions): 1.2s (5020.85 ops/s, 83.68 i/s)
+                    lex and parse: 5.0s (1197.32 ops/s, 19.96 i/s)
+                        render: 1.4s (4152.92 ops/s, 69.22 i/s)
+            lex, parse and render: 6.5s (922.08 ops/s, 15.37 i/s)
+```
+
+And PyPy3.7 gives us a decent increase in performance.
+
+```text
+Best of 5 rounds with 100 iterations per round and 60 ops per iteration (6000 ops per round).
+
+lex template (not expressions): 0.58s (10308.67 ops/s, 171.81 i/s)
+                    lex and parse: 3.6s (1661.20 ops/s, 27.69 i/s)
+                        render: 0.95s (6341.14 ops/s, 105.69 i/s)
+            lex, parse and render: 4.6s (1298.18 ops/s, 21.64 i/s)
+```
+
+On the same machine, running `rake benchmark:run` from the root of the reference implementation source tree gives us these results.
+
+```text
+/usr/bin/ruby ./performance/benchmark.rb lax
+
+Running benchmark for 10 seconds (with 5 seconds warmup).
+
+Warming up --------------------------------------
+                parse:     3.000  i/100ms
+            render:     8.000  i/100ms
+    parse & render:     2.000  i/100ms
+Calculating -------------------------------------
+                parse:     39.072  (± 0.0%) i/s -    393.000  in  10.058789s
+            render:     86.995  (± 1.1%) i/s -    872.000  in  10.024951s
+    parse & render:     26.139  (± 0.0%) i/s -    262.000  in  10.023365s
+```
+
+I've tried to match the benchmark workload to that of the reference implementation, so that we might compare results directly. The workload is meant to be representative of Shopify's use case, although I wouldn't be surprised if their usage has changed subtly since the benchmark fixture was designed.
+
+## Contributing
+
+Please see [Contributing to Python Liquid](https://github.com/jg-rp/liquid/blob/main/contributing.md).
diff --git a/README.rst b/README.rst
index 6adf7ec8..ebfa5c47 100644
--- a/README.rst
+++ b/README.rst
@@ -1,156 +1,1 @@
-.. _Pipenv: https://pipenv.pypa.io/en/latest/
-
-Python Liquid
-=============
-
-A Python implementation of `Liquid <https://shopify.github.io/liquid/>`_, the safe, customer-facing
-template language for flexible web apps.
-
-.. image:: https://img.shields.io/pypi/v/python-liquid?style=flat-square
-    :target: https://pypi.org/project/python-liquid/
-    :alt: Version
-
-.. image:: https://img.shields.io/conda/vn/conda-forge/python-liquid?style=flat-square
-    :target: https://anaconda.org/conda-forge/python-liquid
-    :alt: conda-forge
-
-.. image:: https://img.shields.io/github/actions/workflow/status/jg-rp/liquid/tests.yaml?branch=main&label=tests&style=flat-square
-    :target: https://github.com/jg-rp/liquid/actions/workflows/tests.yaml
-    :alt: Tests
-
-.. image:: https://img.shields.io/github/actions/workflow/status/jg-rp/liquid/coverage.yaml?branch=main&label=coverage&style=flat-square
-    :target: https://github.com/jg-rp/liquid/actions/workflows/coverage.yaml
-    :alt: Coverage
-
-.. image:: https://img.shields.io/pypi/l/python-liquid.svg?style=flat-square
-    :target: https://pypi.org/project/python-liquid/
-    :alt: License
-
-.. image:: https://img.shields.io/pypi/pyversions/python-liquid.svg?style=flat-square
-    :target: https://pypi.org/project/python-liquid/
-    :alt: Python versions
-
-.. image:: https://img.shields.io/badge/pypy-3.7%20%7C%203.8%20%7C%203.9-blue?style=flat-square
-    :target: https://pypi.org/project/python-liquid/
-    :alt: PyPy versions
-
-.. code-block:: python
-
-    from liquid import Template
-
-    template = Template("Hello, {{ you }}!")
-    print(template.render(you="World"))  # "Hello, World!"
-    print(template.render(you="Liquid"))  # "Hello, Liquid!"
-
-
-Installing
-----------
-
-Install Python Liquid using `Pipenv`_:
-
-.. code-block:: text
-
-    $ pipenv install python-liquid
-
-Or `pip <https://pip.pypa.io/en/stable/getting-started/>`_:
-
-.. code-block:: text
-
-    $ python -m pip install -U python-liquid
-
-Or from `conda-forge <https://anaconda.org/conda-forge/python-liquid>`_:
-
-.. code-block:: text
-
-    $ conda install -c conda-forge python-liquid
-
-Links
------
-
-- Documentation: https://jg-rp.github.io/liquid/
-- Change Log: https://github.com/jg-rp/liquid/blob/main/CHANGES.rst
-- PyPi: https://pypi.org/project/python-liquid/
-- Source Code: https://github.com/jg-rp/liquid
-- Issue Tracker: https://github.com/jg-rp/liquid/issues
-
-Related Projects
-----------------
-
-- `liquid-babel <https://github.com/jg-rp/liquid-babel>`_: Internationalization and localization
-  for Liquid templates.
-- `LiquidScript <https://github.com/jg-rp/liquidscript>`_: A JavaScript and TypeScript engine for
-  Liquid with a similar high-level API to Python Liquid.
-- `django-liquid <https://github.com/jg-rp/django-liquid>`_: A Django template backend for Liquid.
-  Render Liquid templates in your Django apps.
-- `Flask-Liquid <https://github.com/jg-rp/Flask-Liquid>`_: A Flask extension for Liquid. Render
-  Liquid templates in your Flask applications.
-- `golden-liquid <https://github.com/jg-rp/golden-liquid>`_: A test suite for Liquid. See how
-  various Liquid template engines compare to the reference implementation.
-
-Compatibility
--------------
-
-We strive to be 100% compatible with the `reference implementation <https://shopify.github.io/liquid/>`_
-of Liquid, written in Ruby. That is, given an equivalent render context, a template rendered with
-Python Liquid should produce the same output as when rendered with Ruby Liquid.
-
-See the `known issues page <https://jg-rp.github.io/liquid/known_issues>`_ for details of known
-incompatibilities between Python Liquid and Ruby Liquid, and please help by raising an issue
-if you notice an incompatibility.
-
-
-Benchmark
----------
-
-You can run the benchmark using ``make benchmark`` (or ``python -O performance.py`` if
-you don't have ``make``) from the root of the source tree. On my ropey desktop computer
-with a Ryzen 5 1500X and Python 3.11.0, we get the following results.
-
-.. code-block:: text
-
-    Best of 5 rounds with 100 iterations per round and 60 ops per iteration (6000 ops per round).
-
-    lex template (not expressions): 1.2s (5020.85 ops/s, 83.68 i/s)
-                     lex and parse: 5.0s (1197.32 ops/s, 19.96 i/s)
-                            render: 1.4s (4152.92 ops/s, 69.22 i/s)
-             lex, parse and render: 6.5s (922.08 ops/s, 15.37 i/s)
-
-And PyPy3.7 gives us a decent increase in performance.
-
-.. code-block:: text
-
-    Best of 5 rounds with 100 iterations per round and 60 ops per iteration (6000 ops per round).
-
-    lex template (not expressions): 0.58s (10308.67 ops/s, 171.81 i/s)
-                     lex and parse: 3.6s (1661.20 ops/s, 27.69 i/s)
-                            render: 0.95s (6341.14 ops/s, 105.69 i/s)
-             lex, parse and render: 4.6s (1298.18 ops/s, 21.64 i/s)
-
-
-On the same machine, running ``rake benchmark:run`` from the root of the reference
-implementation source tree gives us these results.
-
-.. code-block:: text
-
-    /usr/bin/ruby ./performance/benchmark.rb lax
-
-    Running benchmark for 10 seconds (with 5 seconds warmup).
-
-    Warming up --------------------------------------
-                 parse:     3.000  i/100ms
-                render:     8.000  i/100ms
-        parse & render:     2.000  i/100ms
-    Calculating -------------------------------------
-                 parse:     39.072  (± 0.0%) i/s -    393.000  in  10.058789s
-                render:     86.995  (± 1.1%) i/s -    872.000  in  10.024951s
-        parse & render:     26.139  (± 0.0%) i/s -    262.000  in  10.023365s
-
-
-I've tried to match the benchmark workload to that of the reference implementation, so that we might
-compare results directly. The workload is meant to be representative of Shopify's use case, although
-I wouldn't be surprised if their usage has changed subtly since the benchmark fixture was designed.
-
-Contributing
-------------
-
-Please see `Contributing to Python Liquid <https://github.com/jg-rp/liquid/blob/main/contributing.md>`_.
+Python Liquid's README has moved to `README.md <https://github.com/jg-rp/liquid/blob/main/README.md>`_.
\ No newline at end of file
diff --git a/contributing.md b/contributing.md
index 481a2b4a..1be03039 100644
--- a/contributing.md
+++ b/contributing.md
@@ -10,45 +10,48 @@ Hi. Your contributions and questions are always welcome. Feel free to ask questi
 
 ## Development
 
-Install development dependencies with [Pipenv](https://pipenv.pypa.io/en/latest/).
+We use [hatch](https://hatch.pypa.io/latest/) to manage project dependencies and development environments.
+
+Run tests with the _test_ script.
 
 ```shell
-$ pipenv install --dev
-$ pipenv shell
+$ hatch run test
 ```
 
-Run tests with Python's [unittest](https://docs.python.org/3/library/unittest.html#command-line-interface) module.
+Or, to test against the full matrix of supported Python versions.
 
 ```shell
-$ python -m unittest
+$ hatch run test:test
 ```
 
-Or [pytest](https://docs.pytest.org/en/7.1.x/).
+And to test without the _autoescape_ extra.
 
 ```shell
-$ pytest
+$ hatch run noescape:test
 ```
 
-Check formatting and style with [Pylint](https://github.com/PyCQA/pylint), [Flake8](https://flake8.pycqa.org/en/latest/) and [Black](https://github.com/psf/black).
+Lint with [ruff](https://beta.ruff.rs/docs/).
 
 ```shell
-$ tox -e lint
+$ hatch run lint
 ```
 
 Typecheck with [Mypy](https://mypy.readthedocs.io/en/stable/).
 
-```
-$ tox -e typing
+```shell
+$ hatch run typing
 ```
 
-Always write tests using `unittest.TestCase`. We want our tests to be type-checked and linted too. Check test coverage with [Coverage.py](https://coverage.readthedocs.io/en/6.4.3/).
+Check coverage with pytest-cov.
 
+```shell
+$ hatch run cov
 ```
-$ python -m coverage erase
-$ python -m coverage run -p -m unittest
-$ python -m coverage combine
-$ python -m coverage report --fail-under=98
-$ python -m coverage html
+
+Or generate an HTML coverage report.
+
+```shell
+$ hatch run cov-html
 ```
 
 Then open `htmlcov/index.html` in your browser.
@@ -57,16 +60,16 @@ Then open `htmlcov/index.html` in your browser.
 
 [Documentation](https://jg-rp.github.io/liquid/>) is built using [Docusaurus](https://docusaurus.io/). Find the source in the [docs branch](https://github.com/jg-rp/liquid/tree/docs) of this repository.
 
-The `docs` folder in the root of this repository contains older, sphinx-based docs, still hosted on [Read the Docs](https://liquid.readthedocs.io/en/latest/). The plan is to generate API Documentation for Docusaurus from Python doc comments. The older docs will be kept until such time that better Docusaurus/sphinx integration is available or a workaround is found.
+The `docs` folder in the root of this repository contains docs generated from Python docstrings, hosted on [Read the Docs](https://liquid.readthedocs.io/en/latest/). The plan is to mirror this API documentation to the Docusaurus site with the help of [Griffe](https://mkdocstrings.github.io/griffe/).
 
 ## Style Guides
 
 ### Git Commit Messages
 
-There are no hard rules for git commit messages, although you might like to indicate the type of commit by starting the message with `docs:`, `feat:`, `fix:` or `refactor:`, for example.
+There are no hard rules for git commit messages, although you might like to indicate the type of commit by starting the message with `docs:`, `chore:`, `feat:`, `fix:` or `refactor:`, for example.
 
 ### Python Style
 
 All Python files are formatted using [Black](https://github.com/psf/black), with its default configuration.
 
-Docstrings must use the [Sphinx docstring format](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html).
+Docstrings must use [Google style docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html).
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index d4bb2cbb..00000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line, and also
-# from the environment for the first two.
-SPHINXOPTS    ?=
-SPHINXBUILD   ?= sphinx-build
-SOURCEDIR     = .
-BUILDDIR      = _build
-
-# Put it first so that "make" without argument is like "make help".
-help:
-	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
-	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/analysis.md b/docs/analysis.md
new file mode 100644
index 00000000..8cd2639e
--- /dev/null
+++ b/docs/analysis.md
@@ -0,0 +1,10 @@
+# Template Analysis
+
+::: liquid.static_analysis.TemplateAnalysis
+    handler: python
+
+::: liquid.static_analysis.ContextualTemplateAnalysis
+    handler: python
+
+::: liquid.analyze_tags.TagAnalysis
+    handler: python
\ No newline at end of file
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 00000000..866286c9
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,10 @@
+# Rendering Templates
+
+::: liquid.Environment
+    handler: python
+
+::: liquid.Template
+    handler: python
+
+::: liquid.BoundTemplate
+    handler: python
diff --git a/docs/api.rst b/docs/api.rst
deleted file mode 100644
index 5250a874..00000000
--- a/docs/api.rst
+++ /dev/null
@@ -1,152 +0,0 @@
-
-API Reference
-=============
-
-.. module:: liquid
-
-Also see documentation at https://jg-rp.github.io/liquid/.
-
-Rendering Templates
--------------------
-
-.. autoclass:: Environment([options])
-    :members: from_string, get_template, get_template_async, add_tag, add_filter, analyze_tags, analyze_tags_async, analyze_tags_from_string, parse
-
-    .. attribute:: context_depth_limit
-
-        Class attribute.The maximum number of times a render context can be extended or
-        wrapped before a :class:`liquid.exceptions.ContextDepthError` is raised.
-
-    .. attribute:: local_namespace_limit
-
-        Class attribute. The maximum number of bytes (according to :func:`sys.getsizeof`)
-        allowed in a template's local namespace, per render, before a 
-        :class:`liquid.exceptions.LocalNamespaceLimitError` exception is raised. Note
-        that we only count the size of the local namespace values, not its keys.
-
-    .. attribute:: loop_iteration_limit
-
-        Class attribute. The maximum number of loop iterations allowed before a 
-        :class:`liquid.exceptions.LoopIterationLimitError` is raised.
-
-    .. attribute:: output_stream_limit
-
-        Class attribute. The maximum number of bytes that can be written to a template's
-        output stream, per render, before an :class:`liquid.exceptions.OutputStreamLimitError`
-        exception is raised.
-
-    .. attribute:: undefined
-
-        The undefined type. When an identifier can not be resolved, an instance of
-        `undefined` is returned.
-
-    .. attribute:: strict_filters
-
-        Indicates if an undefined filter should raise an exception or be ignored.
-
-    .. attribute:: autoescape
-
-        Indicates if auto-escape is enabled.
-
-    .. attribute:: tags
-
-        A dictionary mapping tag names to :class:`liquid.tag.Tag` instances.
-       
-    .. attribute:: filters
-
-        A dictionary mapping filter names to callable objects implementing a filter's 
-        behavior.
-
-    .. attribute:: mode
-
-        The current tolerance mode.
-
-    .. attribute:: cache
-
-        The template cache.
-
-    .. attribute:: auto_reload
-
-        Indicates if automatic reloading of templates is enabled.
-
-    .. attribute:: template_class
-
-        :meth:`Environment.get_template` and :meth:`Environment.from_string` return an
-        instance of :attr:`Environment.template_class`. Defaults to
-        :class:`liquid.template.BoundTemplate`.
-
-    .. attribute:: globals
-
-        A dictionary of variables that will be added to the context of every template
-        rendered from the environment.
-
-
-.. autofunction:: Template(source: str, [options])
-
-.. autoclass:: liquid.template.BoundTemplate
-    :members: render, render_async, render_with_context, render_with_context_async, analyze, analyze_async, analyze_with_context, analyze_with_context_async
-
-    .. attribute:: name
-
-        The template's name. As it would have been passed to
-        :meth:`liquid.Environment.get_template`.
-    
-    .. attribute:: globals
-
-        A dictionary of variables that will be added to the context of every template
-        rendered from the environment.
-
-Template Analysis
------------------
-
-.. autoclass:: liquid.template.TemplateAnalysis
-
-.. autoclass:: liquid.template.ContextualTemplateAnalysis
-
-.. autoclass:: liquid.analyze_tags.TagAnalysis
-
-
-Template Loaders
-----------------
-
-.. autoclass:: liquid.loaders.FileSystemLoader
-
-.. autoclass:: liquid.loaders.FileExtensionLoader
-
-.. autoclass:: liquid.loaders.DictLoader
-
-.. autoclass:: liquid.loaders.ChoiceLoader
-
-.. autoclass:: liquid.loaders.BaseLoader
-    :members: get_source, get_source_async
-
-.. autoclass:: liquid.loaders.TemplateSource
-
-
-Undefined Types
----------------
-
-.. autoclass:: liquid.Undefined
-.. autoclass:: liquid.StrictUndefined
-.. autoclass:: liquid.DebugUndefined
-.. autoclass:: liquid.StrictDefaultUndefined
-
-Exceptions
-----------
-
-.. autoclass:: liquid.exceptions.Error
-.. autoclass:: liquid.exceptions.LiquidSyntaxError
-.. autoclass:: liquid.exceptions.LiquidTypeError
-.. autoclass:: liquid.exceptions.LiquidValueError
-.. autoclass:: liquid.exceptions.DisabledTagError
-.. autoclass:: liquid.exceptions.NoSuchFilterFunc
-.. autoclass:: liquid.exceptions.FilterArgumentError
-.. autoclass:: liquid.exceptions.FilterValueError
-.. autoclass:: liquid.exceptions.TemplateNotFound
-.. autoclass:: liquid.exceptions.TemplateTraversalError
-.. autoclass:: liquid.exceptions.ContextDepthError
-.. autoclass:: liquid.exceptions.LocalNamespaceLimitError
-.. autoclass:: liquid.exceptions.LoopIterationLimitError
-.. autoclass:: liquid.exceptions.OutputStreamLimitError
-.. autoclass:: liquid.exceptions.UndefinedError
-
diff --git a/docs/conf.py b/docs/conf.py
deleted file mode 100644
index eefbdb96..00000000
--- a/docs/conf.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# Configuration file for the Sphinx documentation builder.
-#
-# This file only contains a selection of the most common options. For a full
-# list see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
-
-# -- Path setup --------------------------------------------------------------
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#
-
-import os
-import sys
-
-sys.path.insert(0, os.path.abspath(".."))
-
-import liquid
-
-
-# -- Project information -----------------------------------------------------
-
-project = "Liquid"
-copyright = "2021, James"
-author = "James"
-
-# The full version, including alpha/beta/rc tags
-release = liquid.__version__
-version = liquid.__version__
-
-
-# -- General configuration ---------------------------------------------------
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
-extensions = [
-    "sphinx.ext.autodoc",
-    "sphinx.ext.intersphinx",
-]
-
-autodoc_typehints = "description"
-intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ["_templates"]
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
-
-
-# -- Options for HTML output -------------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
-#
-html_theme = "nature"
-
-# 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,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["_static"]
diff --git a/docs/css/style.css b/docs/css/style.css
new file mode 100644
index 00000000..411b2223
--- /dev/null
+++ b/docs/css/style.css
@@ -0,0 +1,27 @@
+/* Indentation. */
+div.doc-contents:not(.first) {
+    padding-left: 25px;
+    border-left: .05rem solid var(--md-typeset-table-color);
+  }
+  
+  /* Mark external links as such. */
+  a.autorefs-external::after {
+    /* https://primer.style/octicons/arrow-up-right-24 */
+    background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="rgb(0, 0, 0)" d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path></svg>');
+    content: ' ';
+  
+    display: inline-block;
+    position: relative;
+    top: 0.1em;
+    margin-left: 0.2em;
+    margin-right: 0.1em;
+  
+    height: 1em;
+    width: 1em;
+    border-radius: 100%;
+    background-color: var(--md-typeset-a-color);
+  }
+  a.autorefs-external:hover::after {
+    background-color: var(--md-accent-fg-color);
+  }
+  
\ No newline at end of file
diff --git a/docs/exceptions.md b/docs/exceptions.md
new file mode 100644
index 00000000..a5b51c84
--- /dev/null
+++ b/docs/exceptions.md
@@ -0,0 +1,46 @@
+# Exceptions
+
+::: liquid.exceptions.Error
+    handler: python
+
+::: liquid.exceptions.LiquidSyntaxError
+    handler: python
+
+::: liquid.exceptions.LiquidTypeError
+    handler: python
+
+::: liquid.exceptions.LiquidValueError
+    handler: python
+
+::: liquid.exceptions.DisabledTagError
+    handler: python
+
+::: liquid.exceptions.NoSuchFilterFunc
+    handler: python
+
+::: liquid.exceptions.FilterArgumentError
+    handler: python
+
+::: liquid.exceptions.FilterValueError
+    handler: python
+
+::: liquid.exceptions.TemplateNotFound
+    handler: python
+
+::: liquid.exceptions.TemplateTraversalError
+    handler: python
+
+::: liquid.exceptions.ContextDepthError
+    handler: python
+
+::: liquid.exceptions.LocalNamespaceLimitError
+    handler: python
+
+::: liquid.exceptions.LoopIterationLimitError
+    handler: python
+
+::: liquid.exceptions.OutputStreamLimitError
+    handler: python
+
+::: liquid.exceptions.UndefinedError
+    handler: python
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..5abacf3b
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,65 @@
+# Python Liquid
+
+A Python implementation of [Liquid](https://shopify.github.io/liquid/), the safe customer-facing template language for flexible web apps.
+
+!!! info
+    See [https://jg-rp.github.io/liquid/](https://jg-rp.github.io/liquid/) for guides, [tag and filter references](https://jg-rp.github.io/liquid/language/tags) and extra, non-standard plugins.
+
+## Install
+
+Install Python Liquid using [Pipenv](https://pipenv.pypa.io/en/latest/):
+
+```shell
+$ pipenv install -u python-liquid
+```
+
+Or [pip](https://pip.pypa.io/en/stable/getting-started/):
+
+```shell
+$ pip install python-liquid
+```
+
+Or from [conda-forge](https://anaconda.org/conda-forge/python-liquid):
+
+```shell
+$ conda install -c conda-forge python-liquid
+```
+
+## Quick Start
+
+Render a template string by creating a `Template` and calling its `render()` method.
+
+```python
+from liquid import Template
+
+template = Template("Hello, {{ you }}!")
+print(template.render(you="World"))  # Hello, World!
+print(template.render(you="Liquid"))  # Hello, Liquid!
+```
+
+Keyword arguments passed to render() are available as variables for templates to use in Liquid expressions.
+
+```python
+from liquid import Template
+
+template = Template(
+    "{% for person in people %}"
+    "Hello, {{ person.name }}!\n"
+    "{% endfor %}"
+)
+
+data = {
+    "people": [
+        {"name": "John"},
+        {"name": "Sally"},
+    ]
+}
+
+print(template.render(**data))
+# Hello, John!
+# Hello, Sally!
+```
+
+## What's next?
+
+Learn how to [configure a Liquid environment](https://jg-rp.github.io/liquid/introduction/getting-started#configure) and [load templates](https://jg-rp.github.io/liquid/introduction/loading-templates) from a file system or database.
diff --git a/docs/index.rst b/docs/index.rst
deleted file mode 100644
index 770e52b7..00000000
--- a/docs/index.rst
+++ /dev/null
@@ -1,93 +0,0 @@
-.. Liquid documentation master file, created by
-   sphinx-quickstart on Sun Mar 21 07:16:31 2021.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-.. _Liquid: https://shopify.github.io/liquid/
-
-Python Liquid
-=============
-
-Release v\ |version|
-
-A Python implementation of `Liquid`_. A non evaling templating language suitable for
-end users.
-
-Also see documentation at https://jg-rp.github.io/liquid/.
-
-Installation
-------------
-
-Install and update using `pip <https://pip.pypa.io/en/stable/quickstart/>`_:
-
-.. code-block:: text
-
-    $ python -m pip install -U python-liquid
-
-Quick Start
------------
-
-Render a template string by creating a ``Template`` and calling its ``render`` method.
-
-    >>> from liquid import Template
-    >>> template = Template("Hello, {{ you }}!")
-    >>> template.render(you="World")
-    'Hello, World!'
-    >>> template.render(you="Dave")         
-    'Hello, Dave!'
-
-Keyword arguments passed to ``render`` are made available as variables for templates
-to use in Liquid expressions.
-
-If you want to use the built-in ``include`` or ``render`` tags, you'll need to create an 
-``Environment``, with a template ``Loader``, then load and render templates from that
-environment.
-
-This example assumes a folder called ``templates`` exists in the current working
-directory, and that the template file ``index.html`` exists within it.
-
-    >>> from liquid import Environment, FileSystemLoader
-    >>> env = Environment(loader=FileSystemLoader("templates/"))
-    >>> template = env.get_template("index.html")
-    >>> template.render(some="variable", other="thing")
-
-You can create templates from strings using an ``Environment`` too. This is often more
-efficient than using ``Template`` directly.
-
-    >>> from liquid import Environment
-    >>> env = Environment()
-    >>> template = env.from_string(""" 
-    ...     <html>    
-    ...     {% for i in (1..3) -%}
-    ...     <p>hello {{ some }} {{ i }}</p>
-    ...     {% endfor -%}
-    ...     </html>
-    ... """)
-    >>> print(template.render(some="thing"))
-    <html>
-    <p>hello thing 1</p>
-    <p>hello thing 2</p>
-    <p>hello thing 3</p>
-    </html>
-
-
-User's Guide
-------------
-
-See documentation at https://jg-rp.github.io/liquid/.
-
-API Reference
--------------
-
-.. toctree::
-   :maxdepth: 2
-
-   api
-
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/docs/loaders.md b/docs/loaders.md
new file mode 100644
index 00000000..7f37b76d
--- /dev/null
+++ b/docs/loaders.md
@@ -0,0 +1,20 @@
+# Template Loaders
+
+::: liquid.loaders.FileSystemLoader
+    handler: python
+
+::: liquid.loaders.FileExtensionLoader
+    handler: python
+
+::: liquid.loaders.DictLoader
+    handler: python
+
+::: liquid.loaders.ChoiceLoader
+    handler: python
+
+::: liquid.loaders.BaseLoader
+    handler: python
+
+::: liquid.loaders.TemplateSource
+    handler: python
+
diff --git a/docs/make.bat b/docs/make.bat
deleted file mode 100644
index 2119f510..00000000
--- a/docs/make.bat
+++ /dev/null
@@ -1,35 +0,0 @@
-@ECHO OFF
-
-pushd %~dp0
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
-	set SPHINXBUILD=sphinx-build
-)
-set SOURCEDIR=.
-set BUILDDIR=_build
-
-if "%1" == "" goto help
-
-%SPHINXBUILD% >NUL 2>NUL
-if errorlevel 9009 (
-	echo.
-	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
-	echo.installed, then set the SPHINXBUILD environment variable to point
-	echo.to the full path of the 'sphinx-build' executable. Alternatively you
-	echo.may add the Sphinx directory to PATH.
-	echo.
-	echo.If you don't have Sphinx installed, grab it from
-	echo.http://sphinx-doc.org/
-	exit /b 1
-)
-
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-goto end
-
-:help
-%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-
-:end
-popd
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 4cd0ec25..938e2a2e 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,3 +1,6 @@
+black==23.3.0
+mkdocs-material==9.1.6
+mkdocstrings-python==0.9.0
+mkdocstrings==0.21.2
 python-dateutil==2.8.1
-Sphinx==5.0.1
 typing_extensions==4.2.0
\ No newline at end of file
diff --git a/docs/undefined.md b/docs/undefined.md
new file mode 100644
index 00000000..8652fe6f
--- /dev/null
+++ b/docs/undefined.md
@@ -0,0 +1,13 @@
+# Undefined Types
+
+::: liquid.Undefined
+    handler: python
+
+::: liquid.StrictUndefined
+    handler: python
+
+::: liquid.DebugUndefined
+    handler: python
+
+::: liquid.StrictDefaultUndefined
+    handler: python
\ No newline at end of file
diff --git a/liquid/__about__.py b/liquid/__about__.py
new file mode 100644
index 00000000..0a0a43a5
--- /dev/null
+++ b/liquid/__about__.py
@@ -0,0 +1,1 @@
+__version__ = "1.9.0"
diff --git a/liquid/__init__.py b/liquid/__init__.py
index 58c15815..244cc654 100644
--- a/liquid/__init__.py
+++ b/liquid/__init__.py
@@ -1,17 +1,11 @@
-# flake8: noqa
-# pylint: disable=missing-module-docstring
-
-__version__ = "1.9.0"
-
 try:
-    from markupsafe import escape
     from markupsafe import Markup
+    from markupsafe import escape
     from markupsafe import soft_str
 except ImportError:
-    from liquid.exceptions import escape  # type: ignore
     from liquid.exceptions import Markup  # type: ignore
+    from liquid.exceptions import escape  # type: ignore
 
-    # pylint: disable=invalid-name
     soft_str = str  # type: ignore
 
 from .mode import Mode
@@ -42,6 +36,9 @@
 from .analyze_tags import TagAnalysis
 from .analyze_tags import DEFAULT_INNER_TAG_MAP
 
+from .static_analysis import TemplateAnalysis
+from .static_analysis import ContextualTemplateAnalysis
+
 from . import future
 
 __all__ = (
@@ -49,6 +46,7 @@
     "BoundTemplate",
     "ChoiceLoader",
     "Context",
+    "ContextualTemplateAnalysis",
     "DebugUndefined",
     "DEFAULT_INNER_TAG_MAP",
     "DictLoader",
@@ -58,8 +56,8 @@
     "FileExtensionLoader",
     "FileSystemLoader",
     "future",
-    "FutureBoundTemplate",
     "FutureAwareBoundTemplate",
+    "FutureBoundTemplate",
     "FutureContext",
     "is_undefined",
     "Markup",
@@ -69,6 +67,7 @@
     "StrictUndefined",
     "TagAnalysis",
     "Template",
+    "TemplateAnalysis",
     "Token",
     "Undefined",
 )
diff --git a/liquid/analyze_tags.py b/liquid/analyze_tags.py
index 54275319..32f1ae27 100644
--- a/liquid/analyze_tags.py
+++ b/liquid/analyze_tags.py
@@ -1,18 +1,18 @@
 """Analyze template tags from tokenized source text."""
 from __future__ import annotations
-from collections import defaultdict
 
+from collections import defaultdict
+from typing import TYPE_CHECKING
 from typing import Dict
 from typing import Iterable
 from typing import List
 from typing import Mapping
 from typing import Optional
 from typing import Tuple
-from typing import TYPE_CHECKING
 
 from liquid.token import TOKEN_TAG
 
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
     from liquid.token import Token
 
@@ -28,29 +28,29 @@
 TagMap = Dict[str, List[Tuple[str, int]]]
 
 
-class TagAnalysis:  # pylint: disable=too-many-instance-attributes too-few-public-methods
-    """The result of analyzing a template's tags with :meth:`Environment.analyze_tags`
-    or :meth:`Environment.analyze_tags_async`.
+class TagAnalysis:
+    """The result of analyzing a template's tags with `Environment.analyze_tags()`.
 
     Each of the following properties maps tag names to a list of their locations.
     Locations are (template_name, line_number) tuples.
 
-    Note that ``raw`` tags are not included at all. The lexer converts them to text
+    Note that `raw` tags are not included at all. The lexer converts them to text
     tokens before we get a chance to analyze them.
 
     Also be aware that reported `unexpected_tags` don't handle the possibility of an
-    "inner" tag appearing in a partial template (using ``{% include %}``), where
+    "inner" tag appearing in a partial template (using `{% include %}`), where
     appropriate enclosing block tags are in the parent template.
 
-    :ivar all_tags: A mapping of all tags to their locations. Includes "end", "inner"
-        and unknown tags.
-    :ivar tags: A mapping of tag names to their locations. Excludes "end" and "inner"
-        tags.
-    :ivar unclosed_tags: Block tags that don't have a matching "end" tag.
-    :ivar unexpected_tags: Inner tags that are not properly enclosed by appropriate
-        block tags. For example, an ``{% else %}`` that is not enclosed by a
-        ``{% for %}`` or ``{% unless %}`` block.
-    :ivar unknown_tags: Tags that are unknown to the environment.
+    Attributes:
+        all_tags: A mapping of all tags to their locations. Includes "end", "inner"
+            and unknown tags.
+        tags: A mapping of tag names to their locations. Excludes "end" and "inner"
+            tags.
+        unclosed_tags: Block tags that don't have a matching "end" tag.
+        unexpected_tags: Inner tags that are not properly enclosed by appropriate
+            block tags. For example, an `{% else %}` that is not enclosed by a
+            `{% for %}` or `{% unless %}` block.
+        unknown_tags: Tags that are unknown to the environment.
     """
 
     def __init__(
@@ -94,12 +94,11 @@ def _all_tags(self, tokens: List[Token]) -> TagMap:
                 tags[token.value].append((self.template_name, token.linenum))
         return dict(tags)
 
-    def _audit_tags(
+    def _audit_tags(  # noqa: PLR0912
         self,
         env: Environment,
         tokens: List[Token],
     ) -> Tuple[TagMap, TagMap, TagMap]:
-        # pylint: disable=too-many-locals too-many-branches
         block_stack: List[_BlockStackItem] = []
         unclosed_tags: TagMap = defaultdict(list)
         unexpected_tags: TagMap = defaultdict(list)
@@ -170,23 +169,24 @@ def _audit_tags(
         for tag_name, locations in self.all_tags.items():
             if tag_name.startswith("end"):
                 start_tag_name = tag_name[3:]
-                if start_tag_name in inline_tags and start_tag_name not in unknown_tags:
-                    unknown_tags[tag_name].extend(locations)
-                elif (
-                    tag_name not in registered_end_blocks
+                if (
+                    start_tag_name in inline_tags
                     and start_tag_name not in unknown_tags
+                    or (
+                        tag_name not in registered_end_blocks
+                        and start_tag_name not in unknown_tags
+                    )
                 ):
                     unknown_tags[tag_name].extend(locations)
 
         return dict(unclosed_tags), dict(unexpected_tags), dict(unknown_tags)
 
     def _valid_inner_tag(
-        self, tag_names: Iterable[str], block_stack: List[_BlockStackItem]
+        self,
+        tag_names: Iterable[str],
+        block_stack: List[_BlockStackItem],
     ) -> bool:
-        for tag_name in tag_names:
-            if tag_name in block_stack:  # type: ignore
-                return True
-        return False
+        return any(tag_name in block_stack for tag_name in tag_names)
 
 
 class _BlockStackItem:
diff --git a/liquid/ast.py b/liquid/ast.py
index 2e38926c..2536e559 100644
--- a/liquid/ast.py
+++ b/liquid/ast.py
@@ -2,7 +2,6 @@
 
 from abc import ABC
 from abc import abstractmethod
-
 from typing import Collection
 from typing import Dict
 from typing import List
@@ -13,12 +12,14 @@
 from typing_extensions import Literal
 
 from liquid.context import Context
-from liquid.expression import Expression
-
-from liquid.token import Token, TOKEN_TAG, TOKEN_ILLEGAL
-
 from liquid.exceptions import DisabledTagError
 from liquid.exceptions import Error
+from liquid.expression import Expression
+from liquid.token import TOKEN_ILLEGAL
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
+
+# ruff: noqa: D102
 
 IllegalToken = Token(-1, TOKEN_ILLEGAL, "")
 
@@ -53,11 +54,10 @@ def render(self, context: Context, buffer: TextIO) -> Optional[bool]:
         return self.render_to_output(context, buffer)
 
     async def render_async(self, context: Context, buffer: TextIO) -> Optional[bool]:
-        """An async version of :meth:`liquid.ast.Node.render`."""
+        """An async version of `liquid.ast.Node.render`."""
         if context.disabled_tags:
             self.raise_for_disabled(context.disabled_tags)
         if hasattr(self, "render_to_output_async"):
-            # pylint: disable=no-member
             return await self.render_to_output_async(context, buffer)
         return self.render_to_output(context, buffer)
 
@@ -74,7 +74,7 @@ async def render_to_output_async(
         context: Context,
         buffer: TextIO,
     ) -> Optional[bool]:
-        """An async version of :meth:`liquid.ast.Node.render_to_output`."""
+        """An async version of `liquid.ast.Node.render_to_output`."""
         return self.render_to_output(context, buffer)
 
     def children(self) -> List["ChildNode"]:
@@ -85,31 +85,25 @@ def children(self) -> List["ChildNode"]:
 class ChildNode(NamedTuple):
     """An AST node and expression pair with optional scope and load data.
 
-    :param linenum: The line number of the child's first token in the template source
-        text.
-    :type linenum: int
-    :param expression: An :class:`liquid.expression.Expression`. If not ``None``, this
-        expression is expected to be related to the given :class:`liquid.ast.Node`.
-    :type expression: Optional[liquid.expression.Expression]
-    :param node: A :class:`liquid.ast.Node`. Typically a ``BlockNode`` or
-        ``ConditionalBlockNode``.
-    :type node: Optional[liquid.ast.Node]
-    :param template_scope: A list of names the parent node adds to the template "local"
-        scope. For example, the built-in ``assign``, ``capture``, ``increment`` and
-        ``decrement`` tags all add names to the template scope. This helps us identify,
-        through static analysis, names that are assumed to be "global".
-    :type template_scope: Optional[List[str]]
-    :param block_scope: A list of names available to the given child node. For example,
-        the ``for`` tag adds the name "forloop" for the duration of its block.
-    :type block_scope: Optional[List[str]]
-    :param load_mode: If not ``None``, indicates that the given expression should be
-        used to load a partial template. In "render" mode, the partial will be analyzed
-        in an isolated namespace, without access to the parent's template local scope.
-        In "include" mode, the partial will have access to the parents template local
-        scope and the parent's scope can be updated by the partial template too.
-    :type load_mode: Optional[Literal["render", "include", "extends"]]
-    :param load_context: Meta data a template ``Loader`` might need to find the source
-        of a partial template.
+    Args:
+        linenum: The line number of the child's first token in the template source text.
+        expression: An `liquid.expression.Expression`. If not `None`, this expression is
+            expected to be related to the given `liquid.ast.Node`.
+        node: A `liquid.ast.Node`. Typically a `BlockNode` or `ConditionalBlockNode`.
+        template_scope: A list of names the parent node adds to the template "local"
+            scope. For example, the built-in `assign`, `capture`, `increment` and
+            `decrement` tags all add names to the template scope. This helps us
+            identify, through static analysis, names that are assumed to be "global".
+        block_scope: A list of names available to the given child node. For example,
+            the `for` tag adds the name "forloop" for the duration of its block.
+        load_mode: If not `None`, indicates that the given expression should be used to
+            load a partial template. In "render" mode, the partial will be analyzed in
+            an isolated namespace, without access to the parent's template local scope.
+            In "include" mode, the partial will have access to the parents template
+            local scope and the parent's scope can be updated by the partial template
+            too.
+        load_context: Meta data a template `Loader` might need to find the source
+            of a partial template.
     """
 
     linenum: int
@@ -135,7 +129,6 @@ def __str__(self) -> str:  # pragma: no cover
     def __repr__(self) -> str:
         return f"ParseTree({self.statements})"
 
-    # pylint: disable=useless-return
     def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
         for stmt in self.statements:
             stmt.render(context, buffer)
@@ -209,8 +202,7 @@ def children(self) -> List["ChildNode"]:
 
 
 class ConditionalBlockNode(Node):
-    """A parse tree node representing a sequence of statements and a conditional
-    expression."""
+    """A node containing a sequence of statements and a conditional expression."""
 
     __slots__ = ("tok", "condition", "block", "forced_output")
 
diff --git a/liquid/builtin/__init__.py b/liquid/builtin/__init__.py
index 1d923e2d..7da772c0 100644
--- a/liquid/builtin/__init__.py
+++ b/liquid/builtin/__init__.py
@@ -1,44 +1,42 @@
-# pylint: disable=missing-module-docstring
-from __future__ import annotations
+from __future__ import annotations  # noqa: D104
 
 from typing import TYPE_CHECKING
 
+from . import illegal
 from . import literal
 from . import statement
-from . import illegal
-
-from .tags import if_tag
-from .tags import comment_tag
-from .tags import unless_tag
-from .tags import case_tag
-from .tags import for_tag
-from .tags import tablerow_tag
-from .tags import capture_tag
-from .tags import cycle_tag
-from .tags import assign_tag
-from .tags import increment_tag
-from .tags import decrement_tag
-from .tags import echo_tag
-from .tags import liquid_tag
-from .tags import include_tag
-from .tags import render_tag
-from .tags import ifchanged_tag
-from .tags import inline_comment_tag
-
+from .filters.array import compact
+from .filters.array import concat
+from .filters.array import first
+from .filters.array import join
+from .filters.array import last
+from .filters.array import map_
+from .filters.array import reverse
+from .filters.array import sort
+from .filters.array import sort_natural
+from .filters.array import uniq
+from .filters.array import where
+from .filters.extra import safe
 from .filters.math import abs_
-from .filters.math import at_most
 from .filters.math import at_least
+from .filters.math import at_most
 from .filters.math import ceil
 from .filters.math import divided_by
 from .filters.math import floor
 from .filters.math import minus
+from .filters.math import modulo
 from .filters.math import plus
 from .filters.math import round_
 from .filters.math import times
-from .filters.math import modulo
-
-from .filters.string import capitalize
+from .filters.misc import date
+from .filters.misc import default
+from .filters.misc import size
 from .filters.string import append
+from .filters.string import base64_decode
+from .filters.string import base64_encode
+from .filters.string import base64_url_safe_decode
+from .filters.string import base64_url_safe_encode
+from .filters.string import capitalize
 from .filters.string import downcase
 from .filters.string import escape
 from .filters.string import escape_once
@@ -51,47 +49,40 @@
 from .filters.string import replace
 from .filters.string import replace_first
 from .filters.string import replace_last
+from .filters.string import rstrip
 from .filters.string import slice_
 from .filters.string import split
-from .filters.string import upcase
 from .filters.string import strip
-from .filters.string import rstrip
 from .filters.string import strip_html
 from .filters.string import strip_newlines
 from .filters.string import truncate
 from .filters.string import truncatewords
-from .filters.string import url_encode
+from .filters.string import upcase
 from .filters.string import url_decode
-from .filters.string import base64_encode
-from .filters.string import base64_decode
-from .filters.string import base64_url_safe_encode
-from .filters.string import base64_url_safe_decode
-
-from .filters.array import join
-from .filters.array import first
-from .filters.array import last
-from .filters.array import concat
-from .filters.array import map_
-from .filters.array import reverse
-from .filters.array import sort
-from .filters.array import sort_natural
-from .filters.array import where
-from .filters.array import uniq
-from .filters.array import compact
-
-from .filters.misc import size
-from .filters.misc import default
-from .filters.misc import date
-
-from .filters.extra import safe
-
+from .filters.string import url_encode
+from .tags import assign_tag
+from .tags import capture_tag
+from .tags import case_tag
+from .tags import comment_tag
+from .tags import cycle_tag
+from .tags import decrement_tag
+from .tags import echo_tag
+from .tags import for_tag
+from .tags import if_tag
+from .tags import ifchanged_tag
+from .tags import include_tag
+from .tags import increment_tag
+from .tags import inline_comment_tag
+from .tags import liquid_tag
+from .tags import render_tag
+from .tags import tablerow_tag
+from .tags import unless_tag
 
 if TYPE_CHECKING:
     from liquid import Environment
 
 
-# pylint: disable=too-many-statements
-def register(env: Environment) -> None:
+def register(env: Environment) -> None:  # noqa: PLR0915
     """Register all built-in tags and filters with an environment."""
     env.add_tag(literal.Literal)
     env.add_tag(statement.Statement)
diff --git a/liquid/builtin/drops/__init__.py b/liquid/builtin/drops/__init__.py
index 4fdc4af8..6654ce6b 100644
--- a/liquid/builtin/drops/__init__.py
+++ b/liquid/builtin/drops/__init__.py
@@ -1,3 +1,3 @@
-# pylint: disable=missing-module-docstring,useless-import-alias
-# flake8: noqa
-from .drops import IterableDrop as IterableDrop
+from .drops import IterableDrop  # noqa: D104
+
+__all__ = ("IterableDrop",)
diff --git a/liquid/builtin/drops/drops.py b/liquid/builtin/drops/drops.py
index 202f3b16..38ff8dcf 100644
--- a/liquid/builtin/drops/drops.py
+++ b/liquid/builtin/drops/drops.py
@@ -1,14 +1,12 @@
 """Legacy iterable drop base class."""
-from typing import Any
-from typing import Collection
 from abc import ABC
 from abc import abstractmethod
+from typing import Any
+from typing import Collection
 
 
-# pylint: disable=too-few-public-methods
 class IterableDrop(ABC, Collection[Any]):
-    """Inherit from this class if you want the drop to be iterable from
-    tags like 'include' and 'render'."""
+    """Base class for drops that can be iterated with `include` and `render` tags."""
 
     @abstractmethod
     def step(self, item: Any) -> None:
diff --git a/liquid/builtin/filters/array.py b/liquid/builtin/filters/array.py
index 0b0977d3..16ffa22b 100644
--- a/liquid/builtin/filters/array.py
+++ b/liquid/builtin/filters/array.py
@@ -5,36 +5,32 @@
 from itertools import chain
 from itertools import islice
 from operator import getitem
-
+from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
 from typing import List
 from typing import Sequence
 from typing import Tuple
 from typing import Union
-from typing import TYPE_CHECKING
 
 try:
     from markupsafe import Markup
 except ImportError:
     from liquid.exceptions import Markup  # type: ignore
 
-from liquid.filter import with_environment
+from liquid.exceptions import FilterArgumentError
+from liquid.exceptions import FilterError
+from liquid.expression import NIL
 from liquid.filter import array_filter
-from liquid.filter import sequence_filter
 from liquid.filter import liquid_filter
-
-from liquid.expression import NIL
-
-from liquid.exceptions import FilterError
-from liquid.exceptions import FilterArgumentError
-
+from liquid.filter import sequence_filter
+from liquid.filter import with_environment
 from liquid.undefined import is_undefined
 
 if TYPE_CHECKING:
     from liquid import Environment
 
-ArrayT = Union[List[Any], Tuple[Any, ...]]  # pylint: disable=invalid-name
+ArrayT = Union[List[Any], Tuple[Any, ...]]
 
 # Send objects with missing keys to the end when sorting a list.
 MAX_CH = chr(0x10FFFF)
@@ -66,7 +62,7 @@ def _getitem(sequence: Any, key: object, default: object = None) -> Any:
 
 
 def _lower(obj: Any) -> str:
-    """Helper for the sort filter"""
+    """Helper for the sort filter."""
     try:
         return str(obj).lower()
     except AttributeError:
@@ -90,7 +86,7 @@ def join(
 
 @liquid_filter
 def first(obj: Any) -> object:
-    """Return the first item of an array"""
+    """Return the first item of an array."""
     if isinstance(obj, str):
         return None
 
@@ -105,7 +101,7 @@ def first(obj: Any) -> object:
 
 @liquid_filter
 def last(obj: Sequence[Any]) -> object:
-    """Return the last item of an array"""
+    """Return the last item of an array."""
     if isinstance(obj, str):
         return None
 
@@ -131,8 +127,7 @@ def concat(sequence: ArrayT, second_array: ArrayT) -> ArrayT:
 
 @liquid_filter
 def map_(sequence: ArrayT, key: object) -> List[object]:
-    """Creates an array of values by extracting the values of a named property
-    from another object."""
+    """Create an array of values from a map."""
     try:
         return [_getitem(itm, str(key), default=NIL) for itm in sequence]
     except TypeError as err:
@@ -154,10 +149,10 @@ def sort(sequence: ArrayT, key: object = None) -> List[object]:
     """
     if key:
         key_func = partial(_getitem, key=str(key), default=MAX_CH)
-        return list(sorted(sequence, key=key_func))
+        return sorted(sequence, key=key_func)
 
     try:
-        return list(sorted(sequence))
+        return sorted(sequence)
     except TypeError as err:
         raise FilterError("can't sort sequence") from err
 
@@ -167,15 +162,14 @@ def sort_natural(sequence: ArrayT, key: object = None) -> List[object]:
     """Sorts items in an array in case-insensitive order."""
     if key:
         item_getter = partial(_getitem, key=str(key), default=MAX_CH)
-        return list(sorted(sequence, key=lambda obj: _lower(item_getter(obj))))
+        return sorted(sequence, key=lambda obj: _lower(item_getter(obj)))
 
-    return list(sorted(sequence, key=_lower))
+    return sorted(sequence, key=_lower)
 
 
 @sequence_filter
 def where(sequence: ArrayT, attr: object, value: object = None) -> List[object]:
-    """Creates an array including only the objects with a given property value,
-    or any truthy value by default."""
+    """Create an array from a map where _attr_ equals _value_."""
     if value is not None and not is_undefined(value):
         return [itm for itm in sequence if _getitem(itm, attr) == value]
 
diff --git a/liquid/builtin/filters/extra.py b/liquid/builtin/filters/extra.py
index 7fc41a2e..62b98f92 100644
--- a/liquid/builtin/filters/extra.py
+++ b/liquid/builtin/filters/extra.py
@@ -1,5 +1,6 @@
 """Filters that don't exist in the reference implementation."""
 from __future__ import annotations
+
 from typing import TYPE_CHECKING
 
 try:
@@ -10,7 +11,7 @@
 from liquid.filter import string_filter
 from liquid.filter import with_environment
 
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
 
 
diff --git a/liquid/builtin/filters/math.py b/liquid/builtin/filters/math.py
index a2458305..5cff7ee9 100644
--- a/liquid/builtin/filters/math.py
+++ b/liquid/builtin/filters/math.py
@@ -1,19 +1,17 @@
 """Maths related filter definitions."""
 
-import math
 import decimal
-
+import math
 from typing import Optional
 from typing import Union
 
 from liquid.context import is_undefined
 from liquid.exceptions import FilterArgumentError
-
 from liquid.filter import math_filter
 from liquid.filter import num_arg
 
-DecimalT = decimal.Decimal  # pylint: disable=invalid-name
-NumberT = Union[float, int]  # pylint: disable=invalid-name
+DecimalT = decimal.Decimal
+NumberT = Union[float, int]
 
 
 @math_filter
diff --git a/liquid/builtin/filters/misc.py b/liquid/builtin/filters/misc.py
index 0a587928..0e78c316 100644
--- a/liquid/builtin/filters/misc.py
+++ b/liquid/builtin/filters/misc.py
@@ -4,10 +4,9 @@
 import datetime
 import decimal
 import functools
-
+from typing import TYPE_CHECKING
 from typing import Any
 from typing import Union
-from typing import TYPE_CHECKING
 
 from dateutil import parser
 
@@ -16,12 +15,10 @@
 except ImportError:
     from liquid.exceptions import Markup  # type: ignore
 
-from liquid.filter import liquid_filter
-from liquid.filter import with_environment
-
 from liquid.exceptions import FilterArgumentError
 from liquid.expression import EMPTY
-
+from liquid.filter import liquid_filter
+from liquid.filter import with_environment
 from liquid.undefined import is_undefined
 
 if TYPE_CHECKING:
@@ -66,7 +63,7 @@ def default(obj: Any, default_: object = "", *, allow_false: bool = False) -> An
 @with_environment
 @liquid_filter
 @functools.lru_cache(maxsize=10)
-def date(
+def date(  # noqa: PLR0912
     dat: Union[datetime.datetime, str, int],
     fmt: str,
     *,
diff --git a/liquid/builtin/filters/string.py b/liquid/builtin/filters/string.py
index a6841f45..ad2e5a9b 100644
--- a/liquid/builtin/filters/string.py
+++ b/liquid/builtin/filters/string.py
@@ -6,34 +6,29 @@
 import html
 import re
 import urllib.parse
-
+from typing import TYPE_CHECKING
 from typing import Any
 from typing import List
 from typing import Optional
 from typing import Union
-from typing import TYPE_CHECKING
 
 try:
-    from markupsafe import escape as markupsafe_escape
     from markupsafe import Markup
+    from markupsafe import escape as markupsafe_escape
     from markupsafe import soft_str
 except ImportError:
-    from liquid.exceptions import escape as markupsafe_escape  # type: ignore
     from liquid.exceptions import Markup  # type: ignore
+    from liquid.exceptions import escape as markupsafe_escape  # type: ignore
 
-    # pylint: disable=invalid-name
     soft_str = str  # type: ignore
 
-from liquid.undefined import is_undefined
 from liquid.exceptions import FilterArgumentError
 from liquid.exceptions import FilterError
-
-from liquid.filter import with_environment
-from liquid.filter import string_filter
 from liquid.filter import liquid_filter
-
+from liquid.filter import string_filter
+from liquid.filter import with_environment
 from liquid.limits import to_int
-
+from liquid.undefined import is_undefined
 from liquid.utils.html import strip_tags
 from liquid.utils.text import truncate_chars
 
@@ -51,8 +46,7 @@ def append(val: str, arg: object) -> str:
 
 @string_filter
 def capitalize(val: str) -> str:
-    """Make sure the first character of a string is upper case and the rest
-    lowercase."""
+    """Make sure the first character of _val_ is upper case and the rest lowercase."""
     return val.capitalize()
 
 
@@ -92,7 +86,7 @@ def lstrip(val: str) -> str:
 @with_environment
 @string_filter
 def newline_to_br(val: str, *, environment: Environment) -> str:
-    """Convert '\\r\\n' or '\\n' to '<br />\\n'."""
+    """Convert LF or CRLF to `<br />`, plus a newline."""
     # The reference implementation was changed to replace "\r\n" as well as "\n",
     # but they don't seem to care about "\r" (Mac OS).
     if environment.autoescape:
@@ -134,22 +128,19 @@ def remove_last(val: str, arg: str) -> str:
 
 @string_filter
 def replace(val: str, seq: str, sub: str = "") -> str:
-    """Replaces every occurrence of the first argument in a string with the second
-    argument."""
+    """Replace occurrences of _seq_ in _val_ with _sub_."""
     return val.replace(soft_str(seq), soft_str(sub))
 
 
 @string_filter
 def replace_first(val: str, seq: str, sub: str = "") -> str:
-    """Replaces the first occurrence of the first argument in a string with the second
-    argument."""
+    """Replace the first occurrence of _seq_ in _val_ with _sub_."""
     return val.replace(soft_str(seq), soft_str(sub), 1)
 
 
 @string_filter
 def replace_last(val: str, seq: str, sub: str) -> str:
-    """Replace the last occurrence of the first argument in a string with the second
-    argument."""
+    """Replace the last occurrence of _seq_ in _val_ with _sub_."""
     try:
         before, _, after = val.rpartition(soft_str(seq))
     except ValueError:
@@ -191,8 +182,7 @@ def _slice_arg(val: Any) -> int:
 
 @liquid_filter
 def slice_(val: Any, start: Any, length: Any = 1) -> Union[str, List[object]]:
-    """Return a substring or subsequence, starting at `start`, containing up to
-    `length` characters or items.
+    """Return the subsequence of _val_ starting at _start_ with up to _length_ chars.
 
     Array-like objects return a list, strings return a substring, all other objects are
     cast to a string before returning a substring.
diff --git a/liquid/builtin/illegal.py b/liquid/builtin/illegal.py
index f12cebb4..7933edf2 100644
--- a/liquid/builtin/illegal.py
+++ b/liquid/builtin/illegal.py
@@ -4,9 +4,8 @@
 from liquid.exceptions import LiquidSyntaxError
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.token import TOKEN_ILLEGAL
 from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_ILLEGAL
 
 
 class Illegal(Tag):
@@ -15,7 +14,7 @@ class Illegal(Tag):
     name = TOKEN_ILLEGAL
     block = False
 
-    def parse(self, stream: TokenStream) -> IllegalNode:
+    def parse(self, stream: TokenStream) -> IllegalNode:  # noqa: D102
         tok = stream.current
         stream.next_token()
 
diff --git a/liquid/builtin/literal.py b/liquid/builtin/literal.py
index 01d3870a..d72fa705 100644
--- a/liquid/builtin/literal.py
+++ b/liquid/builtin/literal.py
@@ -6,13 +6,11 @@
 
 from liquid.ast import ChildNode
 from liquid.ast import Node
-
 from liquid.context import Context
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.token import Token
 from liquid.token import TOKEN_LITERAL
+from liquid.token import Token
 
 
 class LiteralNode(Node):
@@ -29,12 +27,13 @@ def __str__(self) -> str:
     def __repr__(self) -> str:  # pragma: no cover
         return f"LiteralNode(tok={self.tok})"
 
-    # pylint: disable=useless-return
-    def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
+    def render_to_output(  # noqa: D102
+        self, _: Context, buffer: TextIO
+    ) -> Optional[bool]:
         buffer.write(self.tok.value)
         return None
 
-    def children(self) -> List[ChildNode]:
+    def children(self) -> List[ChildNode]:  # noqa: D102
         return []
 
 
@@ -43,5 +42,5 @@ class Literal(Tag):
 
     name = TOKEN_LITERAL
 
-    def parse(self, stream: TokenStream) -> LiteralNode:
+    def parse(self, stream: TokenStream) -> LiteralNode:  # noqa: D102
         return LiteralNode(stream.current)
diff --git a/liquid/builtin/statement.py b/liquid/builtin/statement.py
index fc6ab9eb..adf81be3 100644
--- a/liquid/builtin/statement.py
+++ b/liquid/builtin/statement.py
@@ -5,17 +5,16 @@
 
 from liquid.ast import ChildNode
 from liquid.ast import Node
-
 from liquid.context import Context
 from liquid.expression import Expression
 from liquid.parse import expect
-
 from liquid.stream import TokenStream
 from liquid.stringify import to_liquid_string
 from liquid.tag import Tag
-
-from liquid.token import Token
 from liquid.token import TOKEN_STATEMENT
+from liquid.token import Token
+
+# ruff: noqa: D102
 
 
 class StatementNode(Node):
@@ -35,7 +34,6 @@ def __str__(self) -> str:
     def __repr__(self) -> str:  # pragma: no cover
         return f"StatementNode(tok={self.tok}, expression={self.expression!r})"
 
-    # pylint: disable=useless-return
     def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
         val = self.expression.evaluate(context)
         buffer.write(to_liquid_string(val, context.autoescape))
diff --git a/liquid/builtin/tags/assign_tag.py b/liquid/builtin/tags/assign_tag.py
index 9073032a..4b6f7358 100644
--- a/liquid/builtin/tags/assign_tag.py
+++ b/liquid/builtin/tags/assign_tag.py
@@ -1,26 +1,25 @@
 """Tag and node definition for the built-in "assign" tag."""
 import re
 import sys
-
 from typing import List
 from typing import Optional
 from typing import TextIO
 
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_EXPRESSION
-
 from liquid.ast import ChildNode
 from liquid.ast import Node
-from liquid.expression import Expression
-from liquid.tag import Tag
 from liquid.context import Context
-from liquid.stream import TokenStream
-from liquid.expression import AssignmentExpression
 from liquid.exceptions import LiquidSyntaxError
-
-from liquid.parse import expect
+from liquid.expression import AssignmentExpression
+from liquid.expression import Expression
 from liquid.expressions.common import ASSIGN_IDENTIFIER_PATTERN
+from liquid.parse import expect
+from liquid.stream import TokenStream
+from liquid.tag import Tag
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
+
+# ruff: noqa: D102
 
 RE_ASSIGNMENT = re.compile(rf"^({ASSIGN_IDENTIFIER_PATTERN})\s*=\s*(.+)$")
 
@@ -39,7 +38,7 @@ def __init__(self, tok: Token, expression: AssignmentExpression):
     def __str__(self) -> str:
         return f"var ({self.expression})"
 
-    def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
+    def render_to_output(self, context: Context, _: TextIO) -> Optional[bool]:
         self.expression.evaluate(context)
         return False
 
diff --git a/liquid/builtin/tags/capture_tag.py b/liquid/builtin/tags/capture_tag.py
index 71c67ccb..6c79ad89 100644
--- a/liquid/builtin/tags/capture_tag.py
+++ b/liquid/builtin/tags/capture_tag.py
@@ -2,30 +2,25 @@
 
 import re
 import sys
-
 from io import StringIO
-
 from typing import List
 from typing import Optional
 from typing import TextIO
 
-from liquid import ast
 from liquid import Markup
+from liquid import ast
 from liquid.context import Context
 from liquid.exceptions import LiquidSyntaxError
-
 from liquid.parse import expect
 from liquid.parse import get_parser
-
+from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
 from liquid.token import TOKEN_EOF
 from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
-from liquid.stream import TokenStream
-
+# ruff: noqa: D102
 
 RE_CAPTURE = re.compile(r"^\w[a-zA-Z0-9_\-]*$")
 
diff --git a/liquid/builtin/tags/case_tag.py b/liquid/builtin/tags/case_tag.py
index e63fd8ee..6e1881c3 100644
--- a/liquid/builtin/tags/case_tag.py
+++ b/liquid/builtin/tags/case_tag.py
@@ -1,38 +1,35 @@
 """Tag and node definition for the built-in "case" tag."""
 from __future__ import annotations
-import sys
 
+import sys
+from typing import TYPE_CHECKING
 from typing import List
 from typing import Optional
 from typing import TextIO
-from typing import TYPE_CHECKING
 
+from liquid import ast
 from liquid.expression import BooleanExpression
 from liquid.expression import InfixExpression
-
-from liquid.expressions.common import tokenize_common_expression
 from liquid.expressions.common import parse_common_expression
+from liquid.expressions.common import tokenize_common_expression
 from liquid.expressions.stream import TokenStream as ExpressionTokenStream
-
-from liquid.token import Token
+from liquid.parse import expect
+from liquid.parse import get_parser
+from liquid.tag import Tag
 from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_EOF
 from liquid.token import TOKEN_EXPRESSION
 from liquid.token import TOKEN_OR
 from liquid.token import TOKEN_TAG
-
-
-from liquid.parse import get_parser
-from liquid.parse import expect
-
-from liquid import ast
-from liquid.tag import Tag
-from liquid.context import Context
-from liquid.stream import TokenStream
+from liquid.token import Token
 
 if TYPE_CHECKING:
     from liquid import Environment
+    from liquid.context import Context
     from liquid.expression import Expression
+    from liquid.stream import TokenStream
+
+# ruff: noqa: D102
 
 TAG_CASE = sys.intern("case")
 TAG_ENDCASE = sys.intern("endcase")
@@ -63,10 +60,7 @@ def __init__(
         )
 
     def __str__(self) -> str:
-        if not self.whens:
-            buf = ["if (False) { }"]
-        else:
-            buf = [f"if {self.whens[0]}"]
+        buf = ["if (False) { }"] if not self.whens else [f"if {self.whens[0]}"]
 
         if len(self.whens) > 1:
             for when in self.whens[1:]:
diff --git a/liquid/builtin/tags/comment_tag.py b/liquid/builtin/tags/comment_tag.py
index 3a065d0d..21cddf6f 100644
--- a/liquid/builtin/tags/comment_tag.py
+++ b/liquid/builtin/tags/comment_tag.py
@@ -1,23 +1,21 @@
 """Tag and node definition for the built-in "comment" tag."""
 
 import sys
-
 from typing import List
 from typing import Optional
 from typing import TextIO
 
 from liquid import ast
 from liquid.context import Context
+from liquid.parse import eat_block
+from liquid.parse import expect
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.parse import expect
-from liquid.parse import eat_block
-
-from liquid.token import Token
 from liquid.token import TOKEN_EOF
 from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
+# ruff: noqa: D102
 
 TAG_COMMENT = sys.intern("comment")
 TAG_ENDCOMMENT = sys.intern("endcomment")
@@ -41,8 +39,8 @@ def __str__(self) -> str:
 
     def render_to_output(
         self,
-        context: Context,
-        buffer: TextIO,
+        _: Context,
+        __: TextIO,
     ) -> Optional[bool]:
         return False
 
@@ -69,11 +67,10 @@ def parse(self, stream: TokenStream) -> CommentNode:
 
 
 class CommentTextTag(CommentTag):
-    """An implementation of the built-in "comment" tag that includes
-    comment text in a template's AST.
+    """An implementation of the built-in "comment" tag that retains comment text.
 
-    Some Liquid markup might be stripped out by the lexer, so comment
-    text is not guaranteed to be identical to that in the source document.
+    Some Liquid markup might be stripped out by the lexer, so comment text is not
+    guaranteed to be identical to that in the source document.
     """
 
     name = TAG_COMMENT
diff --git a/liquid/builtin/tags/cycle_tag.py b/liquid/builtin/tags/cycle_tag.py
index c2859b8e..b318bc50 100644
--- a/liquid/builtin/tags/cycle_tag.py
+++ b/liquid/builtin/tags/cycle_tag.py
@@ -1,6 +1,5 @@
 """Tag and node definition for the built-in "cycle" tag."""
 import sys
-
 from typing import Iterable
 from typing import Iterator
 from typing import List
@@ -9,29 +8,26 @@
 
 from liquid.ast import ChildNode
 from liquid.ast import Node
-
 from liquid.context import Context
 from liquid.exceptions import LiquidSyntaxError
 from liquid.expression import Expression
-
+from liquid.expressions import Token as ExprToken
+from liquid.expressions import TokenStream as ExprTokenStream
+from liquid.expressions.common import parse_string_or_identifier
+from liquid.expressions.filtered.lex import tokenize
+from liquid.expressions.filtered.parse import parse_obj
 from liquid.parse import expect
 from liquid.stream import TokenStream
 from liquid.stringify import to_liquid_string
 from liquid.tag import Tag
-
-from liquid.token import Token
-from liquid.token import TOKEN_EXPRESSION
-from liquid.token import TOKEN_EOF
-from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_COLON
-
+from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import Token
 from liquid.undefined import is_undefined
 
-from liquid.expressions import Token as ExprToken
-from liquid.expressions import TokenStream as ExprTokenStream
-from liquid.expressions.common import parse_string_or_identifier
-from liquid.expressions.filtered.lex import tokenize
-from liquid.expressions.filtered.parse import parse_obj
+# ruff: noqa: D102
 
 TAG_CYCLE = sys.intern("cycle")
 
@@ -58,10 +54,7 @@ def __str__(self) -> str:
     def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
         if self.group:
             _group = self.group.evaluate(context)
-            if is_undefined(_group):
-                group_name = "__UNDEFINED"
-            else:
-                group_name = str(_group)
+            group_name = "__UNDEFINED" if is_undefined(_group) else str(_group)
         else:
             group_name = ""
 
@@ -78,10 +71,7 @@ async def render_to_output_async(
     ) -> Optional[bool]:
         if self.group:
             _group = await self.group.evaluate_async(context)
-            if is_undefined(_group):
-                group_name = "__UNDEFINED"
-            else:
-                group_name = str(_group)
+            group_name = "__UNDEFINED" if is_undefined(_group) else str(_group)
         else:
             group_name = ""
 
@@ -127,7 +117,7 @@ def parse(self, stream: TokenStream) -> CycleNode:
         group_name: Optional[Expression] = None
 
         parts = list(split_at_first_colon(tokens))
-        if len(parts) == 2:
+        if len(parts) == 2:  # noqa: PLR2004
             group_name = parse_string_or_identifier(ExprTokenStream(iter(parts[0])))
 
         args: List[Expression] = []
diff --git a/liquid/builtin/tags/decrement_tag.py b/liquid/builtin/tags/decrement_tag.py
index 98d7103a..aa6b4686 100644
--- a/liquid/builtin/tags/decrement_tag.py
+++ b/liquid/builtin/tags/decrement_tag.py
@@ -1,24 +1,23 @@
 """Tag and node definition for the built-in "decrement" tag."""
 
 import sys
-
 from typing import List
-from typing import TextIO
 from typing import Optional
+from typing import TextIO
 
 from liquid import ast
 from liquid.context import Context
+from liquid.expressions import TokenStream as ExprTokenStream
+from liquid.expressions.common import parse_unchained_identifier
+from liquid.expressions.filtered.lex import tokenize
+from liquid.parse import expect
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-from liquid.parse import expect
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
 from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
-from liquid.expressions import TokenStream as ExprTokenStream
-from liquid.expressions.common import parse_unchained_identifier
-from liquid.expressions.filtered.lex import tokenize
+# ruff: noqa: D102
 
 TAG_DECREMENT = sys.intern("decrement")
 
diff --git a/liquid/builtin/tags/echo_tag.py b/liquid/builtin/tags/echo_tag.py
index 4312de05..558034a8 100644
--- a/liquid/builtin/tags/echo_tag.py
+++ b/liquid/builtin/tags/echo_tag.py
@@ -2,20 +2,16 @@
 
 import sys
 
-
 from liquid.ast import Node
 from liquid.builtin.statement import StatementNode
-
 from liquid.expression import NIL
 from liquid.expression import Expression
 from liquid.parse import expect
-
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_EXPRESSION
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
 
 TAG_ECHO = sys.intern("echo")
 
@@ -36,7 +32,7 @@ class EchoTag(Tag):
     def _parse_expression(self, value: str) -> Expression:
         return self.env.parse_filtered_expression_value(value)
 
-    def parse(self, stream: TokenStream) -> Node:
+    def parse(self, stream: TokenStream) -> Node:  # noqa: D102
         expect(stream, TOKEN_TAG, value=TAG_ECHO)
         tok = stream.current
         stream.next_token()
diff --git a/liquid/builtin/tags/for_tag.py b/liquid/builtin/tags/for_tag.py
index faf0a210..59054f4d 100644
--- a/liquid/builtin/tags/for_tag.py
+++ b/liquid/builtin/tags/for_tag.py
@@ -1,34 +1,33 @@
 """Tag and node definition for the built-in "for" tag."""
 from __future__ import annotations
-import sys
 
-from typing import List
-from typing import Optional
+import sys
+from typing import TYPE_CHECKING
 from typing import Any
+from typing import Iterator
+from typing import List
 from typing import Mapping
+from typing import Optional
 from typing import TextIO
-from typing import Iterator
 
+from liquid.ast import BlockNode
 from liquid.ast import ChildNode
 from liquid.ast import Node
-from liquid.ast import BlockNode
-
-from liquid.context import Context
-from liquid.expression import LoopExpression
-
 from liquid.exceptions import BreakLoop
 from liquid.exceptions import ContinueLoop
-
-from liquid.parse import get_parser
 from liquid.parse import expect
-
-from liquid.stream import TokenStream
+from liquid.parse import get_parser
 from liquid.tag import Tag
-
-from liquid.token import Token
 from liquid.token import TOKEN_EXPRESSION
 from liquid.token import TOKEN_TAG
+from liquid.token import Token
+
+if TYPE_CHECKING:
+    from liquid.context import Context
+    from liquid.expression import LoopExpression
+    from liquid.stream import TokenStream
 
+# ruff: noqa: D102
 
 TAG_FOR = sys.intern("for")
 TAG_ENDFOR = sys.intern("endfor")
@@ -41,7 +40,6 @@
 ENDFORELSEBLOCK = frozenset((TAG_ENDFOR,))
 
 
-# pylint: disable=too-many-instance-attributes
 class ForLoop(Mapping[str, object]):
     """Loop helper variables."""
 
@@ -290,8 +288,8 @@ def __str__(self) -> str:
 
     def render_to_output(
         self,
-        context: Context,
-        buffer: TextIO,
+        _: Context,
+        __: TextIO,
     ) -> Optional[bool]:
         raise BreakLoop("break")
 
@@ -312,8 +310,8 @@ def __str__(self) -> str:
 
     def render_to_output(
         self,
-        context: Context,
-        buffer: TextIO,
+        _: Context,
+        __: TextIO,
     ) -> Optional[bool]:
         raise ContinueLoop("continue")
 
diff --git a/liquid/builtin/tags/if_tag.py b/liquid/builtin/tags/if_tag.py
index 65cc201c..6067655e 100644
--- a/liquid/builtin/tags/if_tag.py
+++ b/liquid/builtin/tags/if_tag.py
@@ -1,37 +1,34 @@
 """Tag and node definition for the built-in "if" tag."""
 from __future__ import annotations
-import sys
 
-from typing import Optional
+import sys
+from typing import TYPE_CHECKING
 from typing import List
+from typing import Optional
 from typing import TextIO
-from typing import TYPE_CHECKING
 
-from liquid.token import Token
-from liquid.token import TOKEN_EOF
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_EXPRESSION
-
-from liquid.ast import ChildNode
-from liquid.ast import Node
 from liquid.ast import BlockNode
+from liquid.ast import ChildNode
 from liquid.ast import ConditionalBlockNode
-
+from liquid.ast import IllegalNode
+from liquid.ast import Node
 from liquid.exceptions import LiquidSyntaxError
-
+from liquid.parse import eat_block
 from liquid.parse import expect
 from liquid.parse import get_parser
-from liquid.parse import eat_block
-
-from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.ast import IllegalNode
+from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
 if TYPE_CHECKING:
-    from liquid.context import Context
     from liquid import Environment
+    from liquid.context import Context
     from liquid.expression import Expression
+    from liquid.stream import TokenStream
+
+# ruff: noqa: D102
 
 TAG_IF = sys.intern("if")
 TAG_ENDIF = sys.intern("endif")
diff --git a/liquid/builtin/tags/ifchanged_tag.py b/liquid/builtin/tags/ifchanged_tag.py
index 3487028b..b21012c0 100644
--- a/liquid/builtin/tags/ifchanged_tag.py
+++ b/liquid/builtin/tags/ifchanged_tag.py
@@ -1,29 +1,27 @@
 """Tag and node definition for the built-in "ifchanged" tag."""
 from __future__ import annotations
-import sys
 
+import sys
+from typing import TYPE_CHECKING
 from typing import List
 from typing import Optional
 from typing import TextIO
-from typing import TYPE_CHECKING
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
 
+from liquid.ast import BlockNode
 from liquid.ast import ChildNode
 from liquid.ast import Node
-from liquid.ast import BlockNode
-
 from liquid.parse import expect
 from liquid.parse import get_parser
-
-from liquid.stream import TokenStream
 from liquid.tag import Tag
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
-if TYPE_CHECKING:  # pragma: no cover
-    from liquid.context import Context
+if TYPE_CHECKING:
     from liquid import Environment
+    from liquid.context import Context
+    from liquid.stream import TokenStream
 
+# ruff: noqa: D102
 
 TAG_IFCHANGED = sys.intern("ifchanged")
 TAG_ENDIFCHANGED = sys.intern("endifchanged")
@@ -32,7 +30,7 @@
 
 
 class IfChangedNode(Node):
-    """Parse tree node for the built-in "ifchanged" tag"""
+    """Parse tree node for the built-in "ifchanged" tag."""
 
     __slots__ = ("tok", "block")
 
@@ -77,7 +75,7 @@ def children(self) -> List[ChildNode]:
 
 
 class IfChangedTag(Tag):
-    """The built-in "ifchanged" tag"""
+    """The built-in "ifchanged" tag."""
 
     name = TAG_IFCHANGED
     end = TAG_ENDIFCHANGED
diff --git a/liquid/builtin/tags/include_tag.py b/liquid/builtin/tags/include_tag.py
index 99896759..711943bd 100644
--- a/liquid/builtin/tags/include_tag.py
+++ b/liquid/builtin/tags/include_tag.py
@@ -1,44 +1,39 @@
 """Parse tree node and tag definition for the built in "include" tag."""
 import sys
-
+from typing import Dict
 from typing import List
 from typing import Optional
-from typing import Dict
 from typing import TextIO
 from typing import Tuple
 
 from liquid.ast import ChildNode
 from liquid.ast import Node
-
 from liquid.builtin.drops import IterableDrop
 from liquid.context import Context
-
-from liquid.expression import Expression, Literal
-from liquid.expression import Identifier
-
 from liquid.exceptions import LiquidSyntaxError
+from liquid.expression import Expression
+from liquid.expression import Identifier
+from liquid.expression import Literal
+from liquid.expressions import TokenStream as ExprTokenStream
+from liquid.expressions.common import parse_identifier
+from liquid.expressions.common import parse_string_or_identifier
+from liquid.expressions.common import parse_unchained_identifier
+from liquid.expressions.filtered.parse import parse_obj
+from liquid.expressions.include.lex import tokenize
+from liquid.parse import expect
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.parse import expect
-
-from liquid.token import Token
-from liquid.token import TOKEN_EXPRESSION
-from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_WITH
-from liquid.token import TOKEN_FOR
 from liquid.token import TOKEN_AS
-from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_FOR
+from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_WITH
+from liquid.token import Token
 
-from liquid.expressions import TokenStream as ExprTokenStream
-from liquid.expressions.common import parse_string_or_identifier
-from liquid.expressions.common import parse_unchained_identifier
-from liquid.expressions.common import parse_identifier
-from liquid.expressions.include.lex import tokenize
-from liquid.expressions.filtered.parse import parse_obj
-
+# ruff: noqa: D102
 
 TAG_INCLUDE = sys.intern("include")
 
@@ -125,8 +120,6 @@ def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
     async def render_to_output_async(
         self, context: Context, buffer: TextIO
     ) -> Optional[bool]:
-        """Same as ``render_to_output`` but uses async versions of get_template and
-        render_with_context."""
         name = await self.name.evaluate_async(context)
         template = await context.get_template_with_context_async(
             str(name), tag=self.tag
diff --git a/liquid/builtin/tags/increment_tag.py b/liquid/builtin/tags/increment_tag.py
index 7d4b1adb..c93d4f2f 100644
--- a/liquid/builtin/tags/increment_tag.py
+++ b/liquid/builtin/tags/increment_tag.py
@@ -1,26 +1,24 @@
 """Tag and node definition for the built-in "increment" tag."""
 
 import sys
-
 from typing import List
 from typing import Optional
 from typing import TextIO
 
 from liquid.ast import ChildNode
 from liquid.ast import Node
-
 from liquid.context import Context
+from liquid.expressions import TokenStream as ExprTokenStream
+from liquid.expressions.common import parse_unchained_identifier
+from liquid.expressions.filtered.lex import tokenize
+from liquid.parse import expect
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-from liquid.parse import expect
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
 from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
-from liquid.expressions import TokenStream as ExprTokenStream
-from liquid.expressions.common import parse_unchained_identifier
-from liquid.expressions.filtered.lex import tokenize
+# ruff: noqa: D102
 
 TAG_INCREMENT = sys.intern("increment")
 
diff --git a/liquid/builtin/tags/inline_comment_tag.py b/liquid/builtin/tags/inline_comment_tag.py
index 9269418e..ec6d1ef2 100644
--- a/liquid/builtin/tags/inline_comment_tag.py
+++ b/liquid/builtin/tags/inline_comment_tag.py
@@ -5,10 +5,10 @@
 from liquid.builtin.tags.comment_tag import CommentNode
 from liquid.exceptions import LiquidSyntaxError
 from liquid.parse import expect
+from liquid.stream import TokenStream
 from liquid.tag import Tag
-from liquid.token import TOKEN_TAG
 from liquid.token import TOKEN_EXPRESSION
-from liquid.stream import TokenStream
+from liquid.token import TOKEN_TAG
 
 RE_INVALID_INLINE_COMMENT = re.compile(r"\n\s*[^#\s]")
 
@@ -19,7 +19,7 @@ class InlineCommentTag(Tag):
     block = False
     name = sys.intern("#")
 
-    def parse(self, stream: TokenStream) -> CommentNode:
+    def parse(self, stream: TokenStream) -> CommentNode:  # noqa: D102
         expect(stream, TOKEN_TAG, value=self.name)
         tok = stream.current
         # Empty comment tag?
diff --git a/liquid/builtin/tags/liquid_tag.py b/liquid/builtin/tags/liquid_tag.py
index b9620301..7451f705 100644
--- a/liquid/builtin/tags/liquid_tag.py
+++ b/liquid/builtin/tags/liquid_tag.py
@@ -1,32 +1,28 @@
 """Tag and node definition for the built-in "liquid" tag."""
 
 import sys
-
+from typing import TYPE_CHECKING
 from typing import List
 from typing import Optional
 from typing import TextIO
-from typing import TYPE_CHECKING
 
+from liquid.ast import BlockNode
 from liquid.ast import ChildNode
 from liquid.ast import Node
-from liquid.ast import BlockNode
-
 from liquid.context import Context
 from liquid.lex import get_liquid_expression_lexer
-
 from liquid.parse import expect
 from liquid.parse import get_parser
-
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_EXPRESSION
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
+# ruff: noqa: D102
 
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
 
 TAG_LIQUID = sys.intern("liquid")
diff --git a/liquid/builtin/tags/render_tag.py b/liquid/builtin/tags/render_tag.py
index f8896e77..de58e9e0 100644
--- a/liquid/builtin/tags/render_tag.py
+++ b/liquid/builtin/tags/render_tag.py
@@ -1,49 +1,43 @@
 """Parse tree node and tag definition for the built in "render" tag."""
 import sys
-
+from typing import Dict
 from typing import List
 from typing import Optional
-from typing import Dict
 from typing import TextIO
 from typing import Tuple
 
 from liquid.ast import ChildNode
 from liquid.ast import Node
-
 from liquid.builtin.drops import IterableDrop
 from liquid.builtin.tags.for_tag import ForLoop
 from liquid.builtin.tags.include_tag import TAG_INCLUDE
-
 from liquid.context import Context
 from liquid.context import ReadOnlyChainMap
-
 from liquid.exceptions import LiquidSyntaxError
-
-from liquid.expression import Expression, Literal
+from liquid.expression import Expression
 from liquid.expression import Identifier
-
+from liquid.expression import Literal
+from liquid.expressions import TokenStream as ExprTokenStream
+from liquid.expressions.common import parse_identifier
+from liquid.expressions.common import parse_string_literal
+from liquid.expressions.common import parse_unchained_identifier
+from liquid.expressions.filtered.parse import parse_obj
+from liquid.expressions.include.lex import tokenize
 from liquid.parse import expect
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.token import Token
-from liquid.token import TOKEN_EXPRESSION
-from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_WITH
-from liquid.token import TOKEN_FOR
 from liquid.token import TOKEN_AS
-from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_FOR
+from liquid.token import TOKEN_IDENTIFIER
 from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_WITH
+from liquid.token import Token
 
-from liquid.expressions import TokenStream as ExprTokenStream
-from liquid.expressions.common import parse_string_literal
-from liquid.expressions.common import parse_unchained_identifier
-from liquid.expressions.common import parse_identifier
-from liquid.expressions.include.lex import tokenize
-from liquid.expressions.filtered.parse import parse_obj
-
+# ruff: noqa: D102
 
 TAG_RENDER = sys.intern("render")
 
@@ -153,8 +147,6 @@ def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
     async def render_to_output_async(
         self, context: Context, buffer: TextIO
     ) -> Optional[bool]:
-        """An awaitable version of `render_to_output` that loads templates
-        asynchronously."""
         path = await self.name.evaluate_async(context)
         assert isinstance(path, str)
         template = await context.get_template_with_context_async(path, tag=self.tag)
@@ -316,8 +308,7 @@ def parse(self, stream: TokenStream) -> Node:
 
 
 def parse_argument(stream: ExprTokenStream) -> Tuple[str, Expression]:
-    """Return the next key/value pair from the stream where key and value
-    are separated by a colon."""
+    """Return the next key/value pair from the stream."""
     key = str(parse_unchained_identifier(stream))
     stream.next_token()
     stream.expect(TOKEN_COLON)
diff --git a/liquid/builtin/tags/tablerow_tag.py b/liquid/builtin/tags/tablerow_tag.py
index ded1c8e0..f1f991bd 100644
--- a/liquid/builtin/tags/tablerow_tag.py
+++ b/liquid/builtin/tags/tablerow_tag.py
@@ -1,33 +1,29 @@
 """Tag and node definition for the built-in "tablerow" tag."""
 import sys
-
 from typing import Any
 from typing import Dict
+from typing import Iterator
 from typing import List
 from typing import Mapping
 from typing import Optional
 from typing import TextIO
-from typing import Iterator
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_EXPRESSION
 
+from liquid.ast import BlockNode
 from liquid.ast import ChildNode
 from liquid.ast import Node
-from liquid.ast import BlockNode
-
 from liquid.context import Context
-from liquid.expression import LoopExpression
 from liquid.expression import NIL
+from liquid.expression import LoopExpression
 from liquid.limits import to_int
-
 from liquid.parse import expect
 from liquid.parse import get_parser
-
-from liquid.tag import Tag
 from liquid.stream import TokenStream
+from liquid.tag import Tag
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
+# ruff: noqa: D102
 
 TAG_TABLEROW = sys.intern("tablerow")
 TAG_ENDTABLEROW = sys.intern("endtablerow")
@@ -35,7 +31,6 @@
 END_TAGBLOCK = frozenset((TAG_ENDTABLEROW,))
 
 
-# pylint: disable=too-many-instance-attributes
 class TableRow(Mapping[str, object]):
     """Table row helper variables."""
 
@@ -135,7 +130,7 @@ def col0(self) -> int:
 
     @property
     def col_first(self) -> bool:
-        """True if this is the first column. False otherwise"""
+        """True if this is the first column. False otherwise."""
         return self._col == 1
 
     @property
diff --git a/liquid/builtin/tags/unless_tag.py b/liquid/builtin/tags/unless_tag.py
index d467b4e4..d65cf3ca 100644
--- a/liquid/builtin/tags/unless_tag.py
+++ b/liquid/builtin/tags/unless_tag.py
@@ -1,36 +1,35 @@
 """Tag and node definition for the built-in "unless" tag."""
 from __future__ import annotations
-import sys
 
+import sys
+from typing import TYPE_CHECKING
 from typing import List
 from typing import Optional
 from typing import TextIO
-from typing import TYPE_CHECKING
 from typing import Union
 
-from liquid.ast import ChildNode
-from liquid.ast import Node
 from liquid.ast import BlockNode
+from liquid.ast import ChildNode
 from liquid.ast import ConditionalBlockNode
 from liquid.ast import IllegalNode
-
-from liquid.context import Context
+from liquid.ast import Node
 from liquid.exceptions import LiquidSyntaxError
-from liquid.stream import TokenStream
-from liquid.tag import Tag
-
 from liquid.parse import eat_block
 from liquid.parse import expect
 from liquid.parse import get_parser
-
-from liquid.token import Token
+from liquid.tag import Tag
 from liquid.token import TOKEN_EOF
 from liquid.token import TOKEN_EXPRESSION
 from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
+    from liquid.context import Context
     from liquid.expression import Expression
+    from liquid.stream import TokenStream
+
+# ruff: noqa: D102
 
 TAG_UNLESS = sys.intern("unless")
 TAG_ENDUNLESS = sys.intern("endunless")
diff --git a/liquid/chain_map.py b/liquid/chain_map.py
index 15c389d2..8a821ce6 100644
--- a/liquid/chain_map.py
+++ b/liquid/chain_map.py
@@ -5,7 +5,6 @@
 """
 from collections import deque
 from itertools import chain
-
 from typing import Any
 from typing import Iterator
 from typing import Mapping
@@ -29,13 +28,14 @@ def __iter__(self) -> Iterator[str]:
         return chain(*self._maps)
 
     def __len__(self) -> int:
-        return sum(len(map) for map in self._maps)
+        return sum(len(_map) for _map in self._maps)
 
     def size(self) -> int:
         """Return the number of maps in the chain."""
         return len(self._maps)
 
     def get(self, key: str, default: object = None) -> object:
+        """Return the value for key if key is in the dictionary, else default."""
         try:
             return self[key]
         except KeyError:
diff --git a/liquid/context.py b/liquid/context.py
index 1c2b6886..7a41ffc3 100644
--- a/liquid/context.py
+++ b/liquid/context.py
@@ -8,18 +8,14 @@
 import re
 import sys
 import warnings
-
 from contextlib import contextmanager
-
 from functools import partial
 from functools import reduce
-
-from itertools import cycle
 from io import StringIO
-
+from itertools import cycle
 from operator import getitem
 from operator import mul
-
+from typing import TYPE_CHECKING
 from typing import Any
 from typing import Awaitable
 from typing import Callable
@@ -32,31 +28,29 @@
 from typing import Sequence
 from typing import TextIO
 from typing import Union
-from typing import TYPE_CHECKING
 
 from liquid import Mode
 from liquid.chain_map import ReadOnlyChainMap
-from liquid.output import LimitedStringIO
-
-from liquid.exceptions import NoSuchFilterFunc
 from liquid.exceptions import ContextDepthError
 from liquid.exceptions import Error
 from liquid.exceptions import LocalNamespaceLimitError
 from liquid.exceptions import LoopIterationLimitError
+from liquid.exceptions import NoSuchFilterFunc
 from liquid.exceptions import lookup_warning
-
+from liquid.output import LimitedStringIO
+from liquid.undefined import UNDEFINED
 from liquid.undefined import DebugUndefined
-from liquid.undefined import is_undefined
 from liquid.undefined import StrictDefaultUndefined
 from liquid.undefined import StrictUndefined
 from liquid.undefined import Undefined
-from liquid.undefined import UNDEFINED
+from liquid.undefined import is_undefined
 
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
-    from liquid.template import BoundTemplate
     from liquid.builtin.tags.for_tag import ForLoop
+    from liquid.template import BoundTemplate
 
+# ruff: noqa: D102
 
 __all__ = (
     "Context",
@@ -177,13 +171,12 @@ async def _liquid_last_async(
         raise
 
 
-# pylint: disable=too-many-instance-attributes redefined-builtin too-many-public-methods
 class Context:
     """A template render context.
 
-    A new render context is created automatically each time :meth:`BoundTemplate.render`
-    is called, which includes `globals` set on the bound :class:`liquid.Environment` and
-    :class:`liquid.template.BoundTemplate`.
+    A new render context is created automatically each time `BoundTemplate.render`
+    is called, which includes `globals` set on the bound `liquid.Environment` and
+    `liquid.template.BoundTemplate`.
     """
 
     __slots__ = (
@@ -210,11 +203,10 @@ class Context:
     # leave `template` optional for backwards compatibility reasons only. This might
     # change in Python Liquid version 2.0.
 
-    # pylint: disable=too-many-arguments
     def __init__(
         self,
         env: Environment,
-        globals: Optional[Namespace] = None,
+        globals: Optional[Namespace] = None,  # noqa: A002
         disabled_tags: Optional[List[str]] = None,
         copy_depth: int = 0,
         parent_context: Optional[Context] = None,
@@ -371,7 +363,7 @@ def _resolve(self, name: str, default: object = UNDEFINED) -> Any:
                 return self.env.undefined(name)
             return default
 
-    def filter(self, name: str) -> Callable[..., object]:
+    def filter(self, name: str) -> Callable[..., object]:  # noqa: A003
         """Return the filter function with given name."""
         try:
             filter_func = self.env.filters[name]
@@ -400,8 +392,7 @@ async def get_template_async(self, name: str) -> BoundTemplate:
         return await self.env.get_template_async(name)
 
     def get_template_with_context(self, name: str, **kwargs: str) -> BoundTemplate:
-        """Load a template from the environment, optionally referencing the current
-        render context."""
+        """Load a template referencing the current render context."""
         return self.env.get_template_with_context(self, name, **kwargs)
 
     async def get_template_with_context_async(
@@ -409,8 +400,7 @@ async def get_template_with_context_async(
         name: str,
         **kwargs: str,
     ) -> BoundTemplate:
-        """Load a template from the environment asynchronously, optionally referencing
-        the current render context."""
+        """An async version of `get_template_with_context`."""
         return await self.env.get_template_with_context_async(self, name, **kwargs)
 
     def increment(self, name: str) -> int:
@@ -475,7 +465,7 @@ def extend(
 
     @contextmanager
     def loop(self, namespace: Namespace, forloop: ForLoop) -> Iterator[Context]:
-        """Just like ``Context.extend``, but keeps track of ForLoop objects too."""
+        """Just like `Context.extend`, but keeps track of ForLoop objects too."""
         self.raise_for_loop_limit(forloop.length)
         self.loops.append(forloop)
         with self.extend(namespace) as context:
@@ -492,9 +482,7 @@ def parentloop(self) -> Union[Undefined, object]:
             return self.env.undefined("parentloop")
 
     def raise_for_loop_limit(self, length: int = 1) -> None:
-        """Raise a ``LoopIterationLimitError`` if the product of the loop stack is
-        greater than the configured loop iteration limit.
-        """
+        """Raise a `LoopIterationLimitError` if loop stack is bigger than the limit."""
         if (
             self.env.loop_iteration_limit
             and reduce(
@@ -514,8 +502,10 @@ def copy(
         template: Optional[BoundTemplate] = None,
         block_scope: bool = False,
     ) -> Context:
-        """Return a copy of this context without any local variables or other state
-        for stateful tags."""
+        """Return a copy of this render context.
+
+        Local variables and other state for stateful tags are not copied.
+        """
         if self._copy_depth > self.env.context_depth_limit:
             raise ContextDepthError(
                 "maximum context depth reached, possible recursive render"
@@ -567,15 +557,13 @@ def error(self, exc: Error) -> None:
             )
 
     def get_buffer(self, buf: Optional[TextIO] = None) -> StringIO:
-        """Return a new StringIO object, possibly limited according to the configured
-        output stream limit."""
+        """Return a new StringIO object that respects the configured stream limit."""
         if self.env.output_stream_limit is None:
             return StringIO()
 
         carry = buf.size if isinstance(buf, LimitedStringIO) else 0
         return LimitedStringIO(limit=self.env.output_stream_limit - carry)
 
-    # pylint: disable=too-many-return-statements
     @classmethod
     def getitem(cls, obj: Any, key: Any) -> Any:
         """Item getter with special methods for arrays/lists and hashes/dicts."""
@@ -591,7 +579,6 @@ def getitem(cls, obj: Any, key: Any) -> Any:
 
         return getitem(obj, key)
 
-    # pylint: disable=too-many-return-statements
     @classmethod
     async def getitem_async(cls, obj: Any, key: Any) -> object:
         """Item getter with special methods for arrays/lists and hashes/dicts."""
@@ -632,11 +619,10 @@ class VariableCaptureContext(Context):
     # Used for formatting context path strings.
     re_ident = re.compile(r"^[\w_][\w_\-]*$")
 
-    # pylint: disable=too-many-arguments
     def __init__(
         self,
         env: Environment,
-        globals: Optional[Namespace] = None,
+        globals: Optional[Namespace] = None,  # noqa: A002
         disabled_tags: Optional[List[str]] = None,
         copy_depth: int = 0,
         parent_context: Optional[VariableCaptureContext] = None,
@@ -691,7 +677,7 @@ def decrement(self, name: str) -> int:
         self.root_context.local_references.append(name)
         return super().decrement(name)
 
-    def filter(self, name: str) -> Callable[..., object]:
+    def filter(self, name: str) -> Callable[..., object]:  # noqa: A003
         self.root_context.filters.append(name)
         return super().filter(name)
 
@@ -704,7 +690,7 @@ def _count_reference(self, path: ContextPath, result: object) -> None:
                 if isinstance(elem, int):
                     _path.append(f"[{elem}]")
                 else:
-                    if self.re_ident.match(elem):
+                    if self.re_ident.match(elem):  # noqa: PLR5501
                         if _path:
                             _path.append(f".{elem}")
                         else:
@@ -719,8 +705,7 @@ def _count_reference(self, path: ContextPath, result: object) -> None:
 
 
 class FutureContext(Context):
-    """A render context that addresses some incompatibilities between Python Liquid and
-    Ruby Liquid.
+    """A render context configured for maximum compatibility with the Ruby liquid.
 
     These "fixes" have not been implemented in the default `Context` for the benefit of
     existing Python Liquid users that rely on past behavior.
@@ -743,11 +728,7 @@ async def getitem_async(cls, obj: Any, key: Any) -> object:
         return await super().getitem_async(obj, key)
 
     def cycle(self, group_name: str, args: Sequence[object]) -> object:
-        if group_name:
-            key = group_name
-        else:
-            key = str(args)
-
+        key = group_name if group_name else str(args)
         namespace: Dict[str, int] = self.tag_namespace["cycles"]
         index = namespace.setdefault(key, 0)
         try:
@@ -764,7 +745,7 @@ def cycle(self, group_name: str, args: Sequence[object]) -> object:
 
 
 class FutureVariableCaptureContext(VariableCaptureContext, FutureContext):
-    """A render context that captures information about template variables and filters."""
+    """A context that captures information about template variables and filters."""
 
 
 def get_item(
diff --git a/liquid/environment.py b/liquid/environment.py
index 9570202d..c4c78acb 100644
--- a/liquid/environment.py
+++ b/liquid/environment.py
@@ -1,9 +1,10 @@
 """Shared configuration from which templates can be loaded and parsed."""
 
 from __future__ import annotations
-from functools import lru_cache
-from pathlib import Path
 
+import warnings
+from functools import lru_cache
+from typing import TYPE_CHECKING
 from typing import Any
 from typing import Callable
 from typing import ClassVar
@@ -12,115 +13,123 @@
 from typing import Mapping
 from typing import MutableMapping
 from typing import Optional
-from typing import Type
 from typing import Tuple
-from typing import TYPE_CHECKING
+from typing import Type
 from typing import Union
 
-import warnings
-
-from liquid.context import Undefined
-from liquid.mode import Mode
-from liquid.tag import Tag
-from liquid.template import BoundTemplate
-from liquid.lex import get_lexer
-from liquid.parse import get_parser
-from liquid.stream import TokenStream
+from liquid import ast
+from liquid import builtin
+from liquid import loaders
 from liquid.analyze_tags import InnerTagMap
 from liquid.analyze_tags import TagAnalysis
-from liquid.utils import LRUCache
-
+from liquid.context import Undefined
+from liquid.exceptions import Error
+from liquid.exceptions import LiquidSyntaxError
+from liquid.exceptions import TemplateInheritanceError
+from liquid.exceptions import lookup_warning
 from liquid.expressions import parse_boolean_expression
 from liquid.expressions import parse_boolean_expression_with_parens
 from liquid.expressions import parse_conditional_expression
 from liquid.expressions import parse_conditional_expression_with_parens
 from liquid.expressions import parse_filtered_expression
 from liquid.expressions import parse_loop_expression
+from liquid.lex import get_lexer
+from liquid.mode import Mode
+from liquid.parse import get_parser
+from liquid.stream import TokenStream
+from liquid.template import BoundTemplate
+from liquid.utils import LRUCache
 
-from liquid import ast
-from liquid import builtin
-from liquid import loaders
+if TYPE_CHECKING:
+    from pathlib import Path
 
-from liquid.exceptions import Error
-from liquid.exceptions import LiquidSyntaxError
-from liquid.exceptions import lookup_warning
-from liquid.exceptions import TemplateInheritanceError
-
-if TYPE_CHECKING:  # pragma: no cover
+    from liquid.context import Context
     from liquid.expression import BooleanExpression
     from liquid.expression import FilteredExpression
     from liquid.expression import LoopExpression
-    from liquid.context import Context
+    from liquid.tag import Tag
     from liquid.token import Token
 
 
-# pylint: disable=too-many-instance-attributes
 class Environment:
     """Shared configuration from which templates can be loaded and parsed.
 
-    An ``Environment`` is where you might register custom tags and filters, or store
+    An `Environment` is where you might register custom tags and filters, or store
     global context variables that should be included with every template.
 
-    :param tag_start_string: The sequence of characters indicating the start of a
-        liquid tag. Defaults to ``{%``.
-    :type tag_start_string: str
-    :param tag_end_string: The sequence of characters indicating the end of a liquid
-        tag. Defaults to ``%}``.
-    :type tag_end_string: str
-    :param statement_start_string: The sequence of characters indicating the start of
-        an output statement. Defaults to ``{{``.
-    :type statement_start_string: str
-    :param statement_end_string: The sequence of characters indicating the end of an
-        output statement. Defaults to ``}}``
-    :type statement_end_string: str
-    :param template_comments: If ``True``, enable template comments. Where, by default,
-        anything between ``{#`` and ``#}`` is considered a comment. Defaults to
-        ``False``.
-    :type template_comments: bool
-    :param comment_start_string: The sequence of characters indicating the start of a
-        comment. Defaults to ``{#``. ``template_comments`` must be ``True`` for
-        ``comment_start_string`` to have any effect.
-    :type comment_start_string: str
-    :param comment_end_string: The sequence of characters indicating the end of a
-        comment. Defaults to ``#}``. ``template_comments`` must be ``True`` for
-        ``comment_end_string`` to have any effect.
-    :type comment_end_string: str
-    :param tolerance: Indicates how tolerant to be of errors. Must be one of
-        ``Mode.LAX``, ``Mode.WARN`` or ``Mode.STRICT``. Defaults to ``Mode.STRICT``.
-    :type tolerance: Mode
-    :param loader: A template loader. If you want to use the builtin "render" or
-        "include" tags, a loader must be configured. Defaults to an empty
-        :class:`liquid.loaders.DictLoader`.
-    :type loader: liquid.loaders.BaseLoader
-    :param undefined: A subclass of :class:`Undefined` that represents undefined values.
-        Could be one of the built-in undefined types, :class:`Undefined`,
-        :class:`DebugUndefined` or :class:`StrictUndefined`. Defaults to
-        :class:`Undefined`, an undefined type that silently ignores undefined values.
-    :type undefined: liquid.Undefined
-    :param strict_filters: If ``True``, will raise an exception upon finding an
-        undefined filter. Otherwise undefined filters are silently ignored. Defaults to
-        ``True``.
-    :type strict_filters: bool
-    :param autoescape: If `True`, all context values will be HTML-escaped before output
-        unless they've been explicitly marked as "safe". Requires the package
-        Markupsafe. Defaults to ``False``.
-    :type autoescape: bool
-    :param auto_reload: If `True`, loaders that have an ``uptodate`` callable will
-        reload template source data automatically. For deployments where template
-        sources don't change between service reloads, setting auto_reload to `False` can
-        yield an increase in performance by avoiding calls to ``uptodate``. Defaults to
-        ``True``.
-    :type auto_reload: bool
-    :param cache_size: The capacity of the template cache in number of templates.
-        Defaults to 300. If ``cache_size`` is ``None`` or less than ``1``, it has the
-        effect of setting ``auto_reload`` to ``False``.
-    :type cache_size: int
-    :param expression_cache_size: The capacity of each of the common expression caches.
-        Defaults to ``0``, disabling expression caching.
-    :type expression_cache_size: int
-    :param globals: An optional mapping that will be added to the context of any
-        template loaded from this environment. Defaults to ``None``.
-    :type globals: dict
+    Args:
+        tag_start_string: The sequence of characters indicating the start of a
+            liquid tag.
+        tag_end_string: The sequence of characters indicating the end of a liquid
+            tag.
+        statement_start_string: The sequence of characters indicating the start of
+            an output statement.
+        statement_end_string: The sequence of characters indicating the end of an
+            output statement.
+        template_comments: If `True`, enable template comments. Where, by default,
+            anything between `{#` and `#}` is considered a comment.
+            `False`.
+        comment_start_string: The sequence of characters indicating the start of a
+            comment.
+            `comment_start_string` to have any effect.
+        comment_end_string: The sequence of characters indicating the end of a
+            comment.
+            `comment_end_string` to have any effect.
+        tolerance: Indicates how tolerant to be of errors. Must be one of
+            `Mode.LAX`, `Mode.WARN` or `Mode.STRICT`.
+        loader: A template loader. If you want to use the builtin "render" or
+            "include" tags, a loader must be configured.
+            `liquid.loaders.DictLoader`.
+        undefined: A subclass of `Undefined` that represents undefined values. Could be
+            one of the built-in undefined types, `Undefined`, `DebugUndefined` or
+            `StrictUndefined`. undefined type that silently ignores undefined values.
+        strict_filters: If `True`, will raise an exception upon finding an
+            undefined filter. Otherwise undefined filters are silently ignored. Defaults
+            to `True`.
+        autoescape: If `True`, all context values will be HTML-escaped before output
+            unless they've been explicitly marked as "safe". Requires the package
+            Markupsafe.
+        auto_reload: If `True`, loaders that have an `uptodate` callable will
+            reload template source data automatically. For deployments where template
+            sources don't change between service reloads, setting auto_reload to `False`
+            can yield an increase in performance by avoiding calls to `uptodate`.
+            `.
+        cache_size: The capacity of the template cache in number of templates.
+            . If `cache_size` is `None` or less than `1`, it has the
+            effect of setting `auto_reload` to `False`.
+        expression_cache_size: The capacity of each of the common expression caches.
+            `, disabling expression caching.
+        globals: An optional mapping that will be added to the context of any
+            template loaded from this environment.
+
+    Attributes:
+        context_depth_limit: Class attribute.The maximum number of times a render
+            context can be extended or wrapped before a `ContextDepthError` is raised.
+        local_namespace_limit: Class attribute. The maximum number of bytes (according
+            to sys.getsizeof) allowed in a template's local namespace, per render,
+            before a `LocalNamespaceLimitError` exception is raised. Note that we only
+            count the size of the local namespace values, not its keys.
+        loop_iteration_limit: Class attribute. The maximum number of loop iterations
+            allowed before a  `liquid.exceptions.LoopIterationLimitError` is raised.
+        output_stream_limit: Class attribute. The maximum number of bytes that can be
+            written to a template's output stream, per render, before an
+            `OutputStreamLimitError` exception is raised.
+        undefined: The undefined type. When an identifier can not be resolved, an
+            instance of `undefined` is returned.
+        strict_filters: Indicates if an undefined filter should raise an exception or be
+            ignored.
+        autoescape: Indicates if auto-escape is enabled.
+        tags: A dictionary mapping tag names to `liquid.tag.Tag` instances.
+        filters: A dictionary mapping filter names to callable objects implementing a
+            filter's behavior.
+        mode: The current tolerance mode.
+        cache: The template cache.
+        auto_reload: Indicates if automatic reloading of templates is enabled.
+        template_class: `Environment.get_template` and `Environment.from_string`
+            return an instance of :attr:`Environment.template_class`.
+            `liquid.template.BoundTemplate`.
+        globals: A dictionary of variables that will be added to the context of every
+            template rendered from the environment.
     """
 
     # Maximum number of times a context can be extended or wrapped before raising
@@ -140,12 +149,11 @@ class Environment:
     # raising an OutputStreamLimitError.
     output_stream_limit: ClassVar[Optional[int]] = None
 
-    # Instances of ``template_class`` are returned from ``from_string``,
-    # ``get_template`` and ``get_template_async``. It should be the ``BoundTemplate``
+    # Instances of `template_class` are returned from `from_string`,
+    # `get_template` and `get_template_async`. It should be the `BoundTemplate`
     # class or a subclass of it.
     template_class = BoundTemplate
 
-    # pylint: disable=redefined-builtin too-many-arguments too-many-locals
     def __init__(
         self,
         tag_start_string: str = r"{%",
@@ -160,7 +168,7 @@ def __init__(
         autoescape: bool = False,
         auto_reload: bool = True,
         cache_size: int = 300,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
         template_comments: bool = False,
         comment_start_string: str = "{#",
         comment_end_string: str = "#}",
@@ -174,13 +182,13 @@ def __init__(
         # Automatic tag stripping is not yet implemented. Changing this has no effect.
         self.strip_tags = strip_tags
 
-        # An instance of a template loader implementing ``liquid.loaders.BaseLoader``.
-        # ``get_template()`` will delegate to this loader.
+        # An instance of a template loader implementing `liquid.loaders.BaseLoader`.
+        # `get_template()` will delegate to this loader.
         self.loader = loader or loaders.DictLoader({})
 
         # A mapping of template variable names to python objects. These variables will
         # be added to the global namespace of any template rendered from this
-        # environment using ``from_string`` or ``get_template``.
+        # environment using `from_string` or `get_template`.
         self.globals: Mapping[str, object] = globals or {}
 
         # Extended template comment syntax control.
@@ -192,7 +200,7 @@ def __init__(
             self.comment_end_string = ""
 
         # The undefined type. When an identifier can not be resolved, the returned value
-        # is ``Undefined`` or a subclass of ``Undefined``.
+        # is `Undefined` or a subclass of `Undefined`.
         self.undefined = undefined
 
         # Indicates if an undefined filter should raise an exception or be ignored.
@@ -259,66 +267,59 @@ def tokenizer(self) -> Callable[[str], Iterator[Token]]:
         )
 
     def add_tag(self, tag: Type[Tag]) -> None:
-        """Register a liquid tag with the environment. Built-in tags are registered for
-        you automatically with every new :class:`Environment`.
+        """Register a liquid tag with the environment.
+
+        Built-in tags are registered for you automatically with every new `Environment`.
 
-        :param tag: The tag to register.
-        :type tag: Type[Tag]
+        Args:
+            tag: The tag to register.
         """
         self.tags[tag.name] = tag(self)
 
     def add_filter(self, name: str, func: Callable[..., Any]) -> None:
         """Register a filter function with the environment.
 
-        :param name: The filter's name. Does not need to match the function name. This
-            is what you'll use to apply the filter to an expression in a liquid
-            template.
-        :type name: str
-        :param func: Any callable that accepts at least one argument, the result of the
-            expression the filter is applied to. If the filter needs access to the
-            environment or render context, you probably want to make ``func`` a class
-            that inherits from :class:`liquid.filter.Filter`, and override the
-            ``__call__`` method. All builtin filters are implemented in this way.
-        :type func: Callable[..., Any]
+        Args:
+            name: The filter's name. Does not need to match the function name. This is
+                what you'll use to apply the filter to an expression in a liquid
+                template.
+            func: Any callable that accepts at least one argument, the result of the
+                expression the filter is applied to. If the filter needs access to the
+                environment or render context, you probably want to make `func` a class
+                that inherits from `liquid.filter.Filter`, and override the
+                `__call__` method. All builtin filters are implemented in this way.
         """
         self.filters[name] = func
 
     def parse(self, source: str) -> ast.ParseTree:
-        """Parse the given string as a liquid template.
+        """Parse `source` as a liquid template.
 
-        More often than not you'll want to use :meth:`Environment.from_string` instead.
+        More often than not you'll want to use `Environment.from_string` instead.
         """
         parser = get_parser(self)
         token_iter = self.tokenizer()(source)
         return parser.parse(TokenStream(token_iter))
 
-    # pylint: disable=redefined-builtin
     def from_string(
         self,
         source: str,
         name: str = "",
         path: Optional[Union[str, Path]] = None,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
         matter: Optional[Mapping[str, object]] = None,
     ) -> BoundTemplate:
         """Parse the given string as a liquid template.
 
-        :param source: The liquid template source code.
-        :type source: str
-        :param name: Optional name of the template. Available as ``Template.name``.
-            Defaults to the empty string.
-        :type name: str
-        :param path: Optional path or identifier to the origin of the template. Defaults
-            to ``None``.
-        :type path: str, pathlib.Path
-        :param globals: An optional mapping of context variables made available every
-            time the resulting template is rendered. Defaults to ``None``.
-        :type globals: dict
-        :param matter: Optional mapping containing variables associated with the
-            template. Could be "front matter" or other meta data.
-        :type matter: Optional[Mapping[str, object]]
-        :returns: A parsed template ready to be rendered.
-        :rtype: liquid.template.BoundTemplate
+        Args:
+            source: The liquid template source code.
+            name: Optional name of the template. Available as `Template.name`.
+                 empty string.
+            path: Optional path or identifier to the origin of the template. Defaults
+                to `None`.
+            globals: An optional mapping of context variables made available every
+                time the resulting template is rendered.
+            matter: Optional mapping containing variables associated with the
+                template. Could be "front matter" or other meta data.
         """
         try:
             parse_tree = self.parse(source)
@@ -326,7 +327,7 @@ def from_string(
             err.filename = path
             err.source = source
             raise err
-        except Exception as err:
+        except Exception as err:  # noqa: BLE001
             raise Error("unexpected liquid parsing error") from err
         return self.template_class(
             env=self,
@@ -337,23 +338,21 @@ def from_string(
             matter=matter,
         )
 
-    # pylint: disable=redefined-builtin
     def get_template(
         self,
         name: str,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
     ) -> BoundTemplate:
         """Load and parse a template using the configured loader.
 
-        :param name: The template's name. The loader is responsible for interpreting
-            the name. It could be the name of a file or some other identifier.
-        :param globals: A mapping of context variables made available every time the
-            resulting template is rendered.
-        :returns: A parsed template ready to be rendered.
-        :rtype: liquid.template.BoundTemplate
-        :raises:
-            :class:`liquid.exceptions.TemplateNotFound`: if a template with the given
-            name can not be found.
+        Args:
+            name: The template's name. The loader is responsible for interpreting
+                the name. It could be the name of a file or some other identifier.
+            globals: A mapping of context variables made available every time the
+                resulting template is rendered.
+
+        Raises:
+            TemplateNotFound`: if a template with the given name can not be found.
         """
         if self.cache is not None:
             cached = self.cache.get(name)
@@ -371,9 +370,9 @@ def get_template(
     async def get_template_async(
         self,
         name: str,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
     ) -> BoundTemplate:
-        """An async version of ``get_template``."""
+        """An async version of `get_template`."""
         if self.cache is not None:
             cached = self.cache.get(name)
             if isinstance(cached, BoundTemplate) and (
@@ -392,11 +391,10 @@ async def get_template_async(
     def get_template_with_args(
         self,
         name: str,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
         **kwargs: object,
     ) -> BoundTemplate:
-        """Load and parse a template using the configured loader, optionally
-        passing arbitrary keyword arguments to the loader.
+        """Load and parse a template with arbitrary loader arguments.
 
         This method bypasses the environment's template cache. You should use a caching
         loader instead when the loader required extra keyword arguments.
@@ -406,10 +404,10 @@ def get_template_with_args(
     async def get_template_with_args_async(
         self,
         name: str,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
         **kwargs: object,
     ) -> BoundTemplate:
-        """An async version of :meth:`get_template_with_args`."""
+        """An async version of `get_template_with_args`."""
         return await self.loader.load_with_args_async(self, name, globals, **kwargs)
 
     def get_template_with_context(
@@ -418,10 +416,11 @@ def get_template_with_context(
         name: str,
         **kwargs: str,
     ) -> BoundTemplate:
-        """Load and parse a template using the configured loader, optionally referencing
-        a render context."""
-        # No template caching. How would we know what context variables a loader needs?
-        # A custom loader that uses context could implement its own cache.
+        """Load and parse a template with reference to a render context.
+
+        This method bypasses the environment's template cache. You should consider using
+        a caching loader.
+        """
         return self.loader.load_with_context(context, name, **kwargs)
 
     async def get_template_with_context_async(
@@ -430,7 +429,7 @@ async def get_template_with_context_async(
         name: str,
         **kwargs: str,
     ) -> BoundTemplate:
-        """An async version of :meth:`get_template_with_context`."""
+        """An async version of `get_template_with_context`."""
         return await self.loader.load_with_context_async(context, name, **kwargs)
 
     def analyze_tags_from_string(
@@ -440,24 +439,18 @@ def analyze_tags_from_string(
         *,
         inner_tags: Optional[InnerTagMap] = None,
     ) -> TagAnalysis:
-        """Analyze tags in template source text against those registered with this
-        environment.
+        """Analyze tags in template source text.
 
         Unlike template static or contextual analysis, a tag audit does not parse the
         template source text into an AST, nor does it attempt to load partial templates
-        from ``{% include %}`` or `{% render %}` tags.
-
-        :param source: The source text of the template.
-        :type source: str
-        :param name: A name or identifier for the template. Defaults to "<string>".
-        :type name: str
-        :param inner_tags: A mapping of block tags to a list of allowed "inner" tags for
-            the block. For example, ``{% if %}`` blocks are allowed to contain
-            ``{% elsif %}`` and ``{% else %}`` tags.
-        :type inner_tags: Mapping[str, Iterable[str]]
-        :returns: A tag audit including the location of any unknown tags and any
-            unbalanced block tags.
-        :rtype: :class:`liquid.analyze_tags.TagAnalysis`
+        from `{% include %}` or `{% render %}` tags.
+
+        Args:
+            source: The source text of the template.
+            name: A name or identifier for the template.
+            inner_tags: A mapping of block tags to a list of allowed "inner" tags for
+                the block. For example, `{% if %}` blocks are allowed to contain
+                `{% elsif %}` and `{% else %}` tags.
         """
         return TagAnalysis(
             env=self,
@@ -477,24 +470,20 @@ def analyze_tags(
         """Audit template tags without parsing source text into an abstract syntax tree.
 
         This is useful for identifying unknown, misplaced and unbalanced tags in a
-        template's source text. See also :meth:`liquid.template.BoundTemplate.analyze`.
-
-        :param name: The template's name or identifier, as you would use with
-            :meth:`Environment.get_template`. Use :meth:`Environment.analyze_tags_from_string`
-            to audit tags in template text without using a template loader.
-        :type name: str
-        :param context: An optional render context the loader might use to modify the
-            template search space. If given, uses
-            :meth:`liquid.loaders.BaseLoader.get_source_with_context` from the current
-            loader.
-        :type context: Optional[:class:`liquid.Context`]
-        :param inner_tags: A mapping of block tags to a list of allowed "inner" tags for
-            the block. For example, ``{% if %}`` blocks are allowed to contain
-            ``{% elsif %}`` and ``{% else %}`` tags.
-        :type inner_tags: Mapping[str, Iterable[str]]
-        :returns: A tag audit including the location of any unknown tags and any
-            unbalanced block tags.
-        :rtype: :class:`liquid.analyze_tags.TagAnalysis`
+        template's source text. See also `liquid.template.BoundTemplate.analyze`.
+
+        Args:
+            name: The template's name or identifier, as you would use with
+                `Environment.get_template`. Use `Environment.analyze_tags_from_string`
+                to audit tags in template text without using a template loader.
+            context: An optional render context the loader might use to modify the
+                template search space. If given, uses
+                `liquid.loaders.BaseLoader.get_source_with_context` from the current
+                loader.
+            inner_tags: A mapping of block tags to a list of allowed "inner" tags for
+                the block. For example, `{% if %}` blocks are allowed to contain
+                `{% elsif %}` and `{% else %}` tags.
+            kwargs: Loader context.
         """
         if context:
             template_source = self.loader.get_source_with_context(
@@ -517,7 +506,7 @@ async def analyze_tags_async(
         inner_tags: Optional[InnerTagMap] = None,
         **kwargs: str,
     ) -> TagAnalysis:
-        """An async version of :meth:`Environment.analyze_tags`."""
+        """An async version of `Environment.analyze_tags`."""
         if context:
             template_source = await self.loader.get_source_with_context_async(
                 context=context, template_name=name, **kwargs
@@ -533,9 +522,8 @@ async def analyze_tags_async(
             inner_tags=inner_tags,
         )
 
-    # pylint: disable=redefined-builtin
     def make_globals(
-        self, globals: Optional[Mapping[str, object]] = None
+        self, globals: Optional[Mapping[str, object]] = None  # noqa: A002
     ) -> Dict[str, object]:
         """Combine environment globals with template globals."""
         if globals:
@@ -563,11 +551,12 @@ def error(
             )
 
     def set_expression_cache_size(self, maxsize: int = 0) -> None:
-        """Create or replace cached versions of the common expression parsers. If
-        `maxsize` is less than ``1``, no expression caching will happen.
+        """Create or replace cached versions of the common expression parsers.
+
+        If `maxsize` is less than `1`, no expression caching will happen.
 
-        :param maxsize: The maximum size of each expression cache.
-        :type maxsize: int
+        Args:
+            maxsize: The maximum size of each expression cache.
         """
         self.expression_cache_size = maxsize
         (
@@ -608,7 +597,6 @@ def _get_expression_parsers(
         )
 
 
-# pylint: disable=redefined-builtin too-many-arguments too-many-locals
 @lru_cache(maxsize=10)
 def get_implicit_environment(
     tag_start_string: str,
@@ -623,13 +611,13 @@ def get_implicit_environment(
     autoescape: bool,
     auto_reload: bool,
     cache_size: int,
-    globals: Optional[Mapping[str, object]],
+    globals: Optional[Mapping[str, object]],  # noqa: A002
     template_comments: bool,
     comment_start_string: str,
     comment_end_string: str,
     expression_cache_size: int,
 ) -> Environment:
-    """Return an :class:`Environment` initialized with the given arguments."""
+    """Return an `Environment` initialized with the given arguments."""
     return Environment(
         tag_start_string=tag_start_string,
         tag_end_string=tag_end_string,
@@ -651,13 +639,12 @@ def get_implicit_environment(
     )
 
 
-# ``Template`` is a factory function masquerading as a class. The desire to have an
+# `Template` is a factory function masquerading as a class. The desire to have an
 # intuitive API and to please the static type checker outweighs this abuse of Python
 # naming conventions. At least for now.
 
 
-# pylint: disable=redefined-builtin too-many-arguments invalid-name too-many-locals
-def Template(
+def Template(  # noqa: N802, D417
     source: str,
     tag_start_string: str = r"{%",
     tag_end_string: str = r"%}",
@@ -670,77 +657,59 @@ def Template(
     autoescape: bool = False,
     auto_reload: bool = True,
     cache_size: int = 300,
-    globals: Optional[Mapping[str, object]] = None,
+    globals: Optional[Mapping[str, object]] = None,  # noqa: A002
     template_comments: bool = False,
     comment_start_string: str = "{#",
     comment_end_string: str = "#}",
     expression_cache_size: int = 0,
 ) -> BoundTemplate:
-    """Returns a :class:`liquid.template.BoundTemplate`, automatically creating an
-    :class:`Environment` to bind it to.
-
-    Other than `source`, all arguments are passed to the implicit :class:`Environment`,
-    which might have been cached from previous calls to :func:`Template`.
-
-    :param source: A Liquid template source string.
-    :type source: str
-    :param tag_start_string: The sequence of characters indicating the start of a
-        liquid tag. Defaults to ``{%``.
-    :type tag_start_string: str
-    :param tag_end_string: The sequence of characters indicating the end of a liquid
-        tag. Defaults to ``%}``.
-    :type tag_end_string: str
-    :param statement_start_string: The sequence of characters indicating the start of
-        an output statement. Defaults to ``{{``.
-    :type statement_start_string: str
-    :param statement_end_string: The sequence of characters indicating the end of an
-        output statement. Defaults to ``}}``
-    :type statement_end_string: str
-    :param template_comments: If ``True``, enable template comments. Where, by default,
-        anything between ``{#`` and ``#}`` is considered a comment. Defaults to
-        ``False``.
-    :type template_comments: bool
-    :param comment_start_string: The sequence of characters indicating the start of a
-        comment. Defaults to ``{#``. ``template_comments`` must be ``True`` for
-        ``comment_start_string`` to have any effect.
-    :type comment_start_string: str
-    :param comment_end_string: The sequence of characters indicating the end of a
-        comment. Defaults to ``#}``. ``template_comments`` must be ``True`` for
-        ``comment_end_string`` to have any effect.
-    :type comment_end_string: str
-    :param tolerance: Indicates how tolerant to be of errors. Must be one of
-        `Mode.LAX`, `Mode.WARN` or `Mode.STRICT`. Defaults to ``Mode.STRICT``.
-    :type tolerance: Mode
-    :param undefined: A subclass of :class:`Undefined` that represents undefined values.
-        Could be one of the built-in undefined types, :class:`Undefined`,
-        :class:`DebugUndefined` or :class:`StrictUndefined`. Defaults to
-        :class:`Undefined`, an undefined type that silently ignores undefined values.
-    :type undefined: Undefined
-    :param strict_filters: If ``True``, will raise an exception upon finding an
-        undefined filter. Otherwise undefined filters are silently ignored. Defaults to
-        ``True``.
-    :type strict_filters: bool
-    :param autoescape: If `True`, all context values will be HTML-escaped before output
-        unless they've been explicitly marked as "safe". Requires the package
-        Markupsafe. Defaults to ``False``.
-    :type autoescape: bool
-    :param auto_reload: If `True`, loaders that have an ``uptodate`` callable will
-        reload template source data automatically. For deployments where template
-        sources don't change between service reloads, setting auto_reload to `False` can
-        yield an increase in performance by avoiding calls to ``uptodate``. Defaults to
-        ``True``.
-    :type auto_reload: bool
-    :param cache_size: The capacity of the template cache in number of templates.
-        Defaults to 300. If ``cache_size`` is ``None`` or less than ``1``, it has the
-        effect of setting ``auto_reload`` to ``False``.
-    :type cache_size: int
-    :param expression_cache_size: The capacity of each of the common expression caches.
-        Defaults to ``0``, disabling expression caching.
-    :type expression_cache_size: int
-    :param globals: An optional mapping that will be added to the context of any
-        template loaded from this environment. Defaults to ``None``.
-    :type globals: dict
-    :rtype: BoundTemplate
+    """Parse a template, automatically creating an `Environment` to bind it to.
+
+    Other than `source`, all arguments are passed to the implicit `Environment`,
+    which might have been cached from previous calls to `Template`.
+
+    Args:
+        source: A Liquid template source string.
+        tag_start_string: The sequence of characters indicating the start of a liquid
+            tag.
+        tag_end_string: The sequence of characters indicating the end of a liquid tag.
+
+        statement_start_string: The sequence of characters indicating the start of an
+            output statement.
+        statement_end_string: The sequence of characters indicating the end of an output
+            statement.
+        template_comments: If `True`, enable template comments. Where, by default,
+            anything between `{#` and `#}` is considered a comment.
+        comment_start_string: The sequence of characters indicating the start of a
+            comment.
+            `comment_start_string` to have any effect.
+        comment_end_string: The sequence of characters indicating the end of a comment.
+            `template_comments` must be `True` for
+            `comment_end_string` to have any effect.
+        tolerance: Indicates how tolerant to be of errors. Must be one of
+            `Mode.LAX`, `Mode.WARN` or `Mode.STRICT`.
+        undefined: A subclass of `Undefined` that represents undefined values. Could be
+            one of the built-in undefined types, `Undefined`, `DebugUndefined` or
+            `StrictUndefined`.
+            ignores undefined values.
+        strict_filters: If `True`, will raise an exception upon finding an undefined
+            filter. Otherwise undefined filters are silently ignored.
+            `True`.
+        autoescape: If `True`, all context values will be HTML-escaped before output
+            unless they've been explicitly marked as "safe". Requires the package
+            Markupsafe.
+        auto_reload: If `True`, loaders that have an `uptodate` callable will reload
+            template source data automatically. For deployments where template sources
+            don't change between service reloads, setting auto_reload to `False` can
+            yield an increase in performance by avoiding calls to `uptodate`. Defaults
+            to `True`.
+        cache_size: The capacity of the template cache in number of templates. Defaults
+            to 300. If `cache_size` is `None` or less than `1`, it has the effect of
+            setting `auto_reload` to `False`.
+        expression_cache_size: The capacity of each of the common expression caches.
+            `, disabling expression caching.
+        globals: An optional mapping that will be added to the context of any template
+            loaded from this environment.
     """
     # Resorting to named arguments (repeated 3 times) as I've twice missed a bug
     # because of positional arguments.
diff --git a/liquid/exceptions.py b/liquid/exceptions.py
index 12bed40c..1152f0a6 100644
--- a/liquid/exceptions.py
+++ b/liquid/exceptions.py
@@ -1,13 +1,12 @@
 """Liquid specific Exceptions and warnings."""
 
+from pathlib import Path
 from typing import Any
 from typing import Dict
 from typing import Optional
 from typing import Type
 from typing import Union
 
-from pathlib import Path
-
 
 class Error(Exception):
     """Base class for all Liquid exceptions."""
@@ -32,20 +31,35 @@ def __str__(self) -> str:
 
     @property
     def message(self) -> object:
-        """Return the exception's error message if one was given."""
+        """The exception's error message if one was given."""
         if self.args:
             return self.args[0]
         return None
 
+    @property
+    def name(self) -> str:
+        """The name of the template that raised this exception.
+
+        An empty string is return if a name is not available.
+        """
+        if isinstance(self.filename, Path):
+            return self.filename.as_posix()
+        if self.filename:
+            return str(self.filename)
+        return ""
+
 
 class LiquidInterrupt(Exception):
-    """Loop interrupt"""
+    """Loop interrupt exception."""
 
 
 class StopRender(Exception):
-    """An interrupt used to signal that ``BoundTemplate.render_with_context`` should
-    stop rendering more nodes. This is used by template inheritance tags and is not
-    an error condition."""
+    """Template inheritance interrupt.
+
+    An interrupt used to signal that `BoundTemplate.render_with_context` should stop
+    rendering more nodes. This is used by template inheritance tags and is not an error
+    condition.
+    """
 
 
 class LiquidEnvironmentError(Error):
@@ -64,16 +78,6 @@ def __init__(
         super().__init__(*args, linenum=linenum, filename=filename)
         self.source: Optional[str] = None
 
-    @property
-    def name(self) -> str:
-        """Return the name of the template that raised this exception. Return an empty
-        string if a name is not available."""
-        if isinstance(self.filename, Path):
-            return self.filename.as_posix()
-        if self.filename:
-            return str(self.filename)
-        return ""
-
 
 class TemplateInheritanceError(Error):
     """An exceptions raised when template inheritance tags are used incorrectly.
@@ -90,16 +94,6 @@ def __init__(
         super().__init__(*args, linenum=linenum, filename=filename)
         self.source: Optional[str] = None
 
-    @property
-    def name(self) -> str:
-        """Return the name of the template that raised this exception. Return an empty
-        string if a name is not available."""
-        if isinstance(self.filename, Path):
-            return self.filename.as_posix()
-        if self.filename:
-            return str(self.filename)
-        return ""
-
 
 class RequiredBlockError(TemplateInheritanceError):
     """An exception raised when a required block has not been overridden."""
@@ -145,14 +139,12 @@ class ResourceLimitError(Error):
 class ContextDepthError(ResourceLimitError):
     """Exception raised when the maximum context depth is reached.
 
-    Usually indicates recursive use of ``render`` or ``include`` tags.
+    Usually indicates recursive use of `render` or `include` tags.
     """
 
 
 class LoopIterationLimitError(ResourceLimitError):
-    """Exception raised when the maximum number of loop iterations has
-    been exceeded.
-    """
+    """Exception raised when the loop iteration limit has been exceeded."""
 
 
 class OutputStreamLimitError(ResourceLimitError):
@@ -160,8 +152,7 @@ class OutputStreamLimitError(ResourceLimitError):
 
 
 class LocalNamespaceLimitError(ResourceLimitError):
-    """Exception raised when the maximum size of a render context's local namespace
-    has been exceeded."""
+    """Exception raised when a local namespace limit has been exceeded."""
 
 
 # LiquidValueError inheriting from LiquidSyntaxError does not make complete sense.
@@ -219,7 +210,7 @@ def lookup_warning(exc: Type[Error]) -> Type[LiquidWarning]:
     return WARNINGS.get(exc, LiquidWarning)
 
 
-def escape(val: Any) -> str:
+def escape(_: Any) -> str:
     """A dummy escape function that always raises an exception."""
     raise Error("autoescape requires Markupsafe to be installed")
 
@@ -231,18 +222,17 @@ def __init__(self, _: object):
         super().__init__()
         raise Error("autoescape requires Markupsafe to be installed")
 
-    def join(self, _: object) -> str:
+    def join(self, _: object) -> str:  # noqa: D102
         raise Error(
             "autoescape requires Markupsafe to be installed"
         )  # pragma: no cover
 
-    # pylint: disable=missing-function-docstring
-    def unescape(self) -> str:
+    def unescape(self) -> str:  # noqa: D102
         raise Error(
             "autoescape requires Markupsafe to be installed"
         )  # pragma: no cover
 
-    def format(self, *args: Any, **kwargs: Any) -> str:
+    def format(self, *args: Any, **kwargs: Any) -> str:  # noqa: A003, D102, ARG002
         raise Error(
             "autoescape requires Markupsafe to be installed"
         )  # pragma: no cover
diff --git a/liquid/expression.py b/liquid/expression.py
index 563029f4..b785bd0a 100644
--- a/liquid/expression.py
+++ b/liquid/expression.py
@@ -1,14 +1,12 @@
 """Liquid expression objects."""
 from __future__ import annotations
-import sys
 
+import sys
 from abc import ABC
 from abc import abstractmethod
-
 from collections import abc
 from decimal import Decimal
 from itertools import islice
-
 from typing import Any
 from typing import Dict
 from typing import Generic
@@ -21,19 +19,16 @@
 from typing import TypeVar
 from typing import Union
 
+from liquid import Markup
 from liquid.context import Context
 from liquid.context import FutureContext
-
-from liquid import Markup
-
-from liquid.exceptions import LiquidTypeError
 from liquid.exceptions import Error
-from liquid.exceptions import NoSuchFilterFunc
 from liquid.exceptions import FilterValueError
-
+from liquid.exceptions import LiquidTypeError
+from liquid.exceptions import NoSuchFilterFunc
 from liquid.limits import to_int
 
-# pylint: disable=missing-class-docstring too-few-public-methods too-many-lines
+# ruff: noqa: D102 D101
 
 
 class Expression(ABC):
@@ -44,7 +39,7 @@ def evaluate(self, context: Context) -> object:
         """Evaluate the expression with the given context."""
 
     async def evaluate_async(self, context: Context) -> object:
-        """An async version of :meth:`liquid.expression.Expression.evaluate`."""
+        """An async version of `liquid.expression.Expression.evaluate`."""
         return self.evaluate(context)
 
     def children(self) -> List[Expression]:
@@ -64,7 +59,7 @@ def __repr__(self) -> str:  # pragma: no cover
     def __str__(self) -> str:  # pragma: no cover
         return ""
 
-    def evaluate(self, context: Context) -> None:
+    def evaluate(self, _: Context) -> None:
         return None
 
     def children(self) -> List[Expression]:
@@ -90,7 +85,7 @@ def __repr__(self) -> str:  # pragma: no cover
     def __str__(self) -> str:  # pragma: no cover
         return "empty"
 
-    def evaluate(self, context: Context) -> Empty:
+    def evaluate(self, _: Context) -> Empty:
         return self
 
     def children(self) -> List[Expression]:
@@ -118,7 +113,7 @@ def __repr__(self) -> str:  # pragma: no cover
     def __str__(self) -> str:  # pragma: no cover
         return "blank"
 
-    def evaluate(self, context: Context) -> Blank:
+    def evaluate(self, _: Context) -> Blank:
         return self
 
     def children(self) -> List[Expression]:
@@ -142,7 +137,7 @@ def __repr__(self) -> str:  # pragma: no cover
     def __str__(self) -> str:  # pragma: no cover
         return "continue"
 
-    def evaluate(self, context: Context) -> int:
+    def evaluate(self, _: Context) -> int:
         return 0
 
     def children(self) -> List[Expression]:
@@ -152,7 +147,7 @@ def children(self) -> List[Expression]:
 CONTINUE = Continue()
 
 
-T = TypeVar("T")  # pylint: disable=invalid-name
+T = TypeVar("T")
 
 
 class Literal(Expression, Generic[T]):
@@ -173,7 +168,7 @@ def __hash__(self) -> int:
     def __sizeof__(self) -> int:
         return sys.getsizeof(self.value)
 
-    def evaluate(self, context: Context) -> object:
+    def evaluate(self, _: Context) -> object:
         return self.value
 
     def children(self) -> List[Expression]:
@@ -499,7 +494,6 @@ def __str__(self) -> str:
         args_str = ", ".join([str(arg) for arg in self.args])
         if args_str:
             buf.append(f": {args_str}")
-            # return f"{self.name}: {args_str})"
 
         kwargs_str = ", ".join([f"{k}: {v}" for k, v in self.kwargs.items()])
         if kwargs_str:
@@ -516,8 +510,7 @@ async def evaluate_args_async(self, context: Context) -> List[object]:
         return [await arg.evaluate_async(context) for arg in self.args]
 
     def evaluate_kwargs(self, context: Context) -> Dict[str, object]:
-        """Return a dictionary of filter keyword arguments evaluated with the given
-        context."""
+        """Return evaluated keyword arguments for this filter."""
         # Shortcut for the common case. Most filters do not use named parameters.
         if not self.kwargs:
             return {}
@@ -576,7 +569,7 @@ def __eq__(self, other: object) -> bool:
         )
 
     def __str__(self) -> str:
-        filter_str = " | ".join([str(filter) for filter in self.filters])
+        filter_str = " | ".join([str(_filter) for _filter in self.filters])
 
         if filter_str:
             return f"{self.expression} | {filter_str}"
@@ -622,7 +615,7 @@ def apply_filters(
                 continue
             except Error:
                 raise
-            except Exception as err:
+            except Exception as err:  # noqa: BLE001
                 raise Error(
                     f"filter '{_filter.name}': unexpected error: {err}"
                 ) from err
@@ -666,7 +659,7 @@ async def apply_filters_async(
                 continue
             except Error:
                 raise
-            except Exception as err:
+            except Exception as err:  # noqa: BLE001
                 raise Error(
                     f"filter '{_filter.name}': unexpected error: {err}"
                 ) from err
@@ -736,7 +729,7 @@ def __str__(self) -> str:
 
         if self.filters:
             buf.append("|")
-            buf.append(" | ".join([str(filter) for filter in self.filters]))
+            buf.append(" | ".join([str(_filter) for _filter in self.filters]))
 
         return " ".join(buf)
 
@@ -954,11 +947,7 @@ def _limit_and_offset(
         if limit is not None:
             length = min(length, limit)
 
-        if offset:
-            stop = offset + length
-        else:
-            stop = length
-
+        stop = offset + length if offset else length
         context.stopindex(key=offset_key, index=stop)
         it = islice(it, offset, stop)
 
@@ -1070,7 +1059,6 @@ def is_truthy(obj: Any) -> bool:
     return _is_py_falsy_number(obj) or obj not in (False, None)
 
 
-# pylint: disable=too-many-return-statements
 def compare_bool(left: Any, operator: str, right: Any) -> bool:
     """Compare an object to a boolean value."""
     if (isinstance(left, bool) and _is_py_falsy_number(right)) or (
@@ -1094,8 +1082,7 @@ def compare_bool(left: Any, operator: str, right: Any) -> bool:
     raise LiquidTypeError(f"unknown operator: {type(left)} {operator} {type(right)}")
 
 
-# pylint: disable=too-many-return-statements,too-many-branches
-def compare(left: Any, operator: str, right: Any) -> bool:
+def compare(left: Any, operator: str, right: Any) -> bool:  # noqa: PLR0911, PLR0912
     """Return the result of a comparison operation between two objects."""
     if operator == "and":
         return is_truthy(left) and is_truthy(right)
@@ -1130,7 +1117,6 @@ def compare(left: Any, operator: str, right: Any) -> bool:
     if type(left) in (int, float) and type(right) in (int, float):
         return eval_number_expression(left, operator, right)
 
-    # pylint: disable=unidiomatic-typecheck
     if type(left) != type(right):
         raise LiquidTypeError(
             f"invalid operator for types '{str(left)} {operator} {str(right)}'"
diff --git a/liquid/expressions/__init__.py b/liquid/expressions/__init__.py
index 34ddfda3..6d78fd8d 100644
--- a/liquid/expressions/__init__.py
+++ b/liquid/expressions/__init__.py
@@ -1,22 +1,18 @@
-# flake8: noqa
-# pylint: disable=useless-import-alias,missing-module-docstring
-from liquid.expressions.common import Token
-from liquid.expressions.stream import TokenStream
-
-from liquid.expressions.arguments import parse_call_arguments
+from liquid.expressions.arguments import parse_call_arguments  # noqa: D104
 from liquid.expressions.arguments import parse_keyword_arguments
 from liquid.expressions.arguments import parse_macro_arguments
 from liquid.expressions.boolean.parse import parse as parse_boolean_expression
 from liquid.expressions.boolean.parse import (
     parse_with_parens as parse_boolean_expression_with_parens,
 )
+from liquid.expressions.common import Token
 from liquid.expressions.conditional.parse import parse as parse_conditional_expression
 from liquid.expressions.conditional.parse import (
     parse_with_parens as parse_conditional_expression_with_parens,
 )
 from liquid.expressions.filtered.parse import parse as parse_filtered_expression
 from liquid.expressions.loop.parse import parse as parse_loop_expression
-
+from liquid.expressions.stream import TokenStream
 
 __all__ = (
     "parse_call_arguments",
diff --git a/liquid/expressions/arguments/__init__.py b/liquid/expressions/arguments/__init__.py
index de48b42c..7a1089ce 100644
--- a/liquid/expressions/arguments/__init__.py
+++ b/liquid/expressions/arguments/__init__.py
@@ -1,5 +1,4 @@
-# pylint: disable=missing-module-docstring
-from .lex import tokenize
+from .lex import tokenize  # noqa: D104
 from .parse import parse_call_arguments
 from .parse import parse_keyword_arguments
 from .parse import parse_macro_arguments
diff --git a/liquid/expressions/arguments/lex.py b/liquid/expressions/arguments/lex.py
index d23c082c..641e28f3 100644
--- a/liquid/expressions/arguments/lex.py
+++ b/liquid/expressions/arguments/lex.py
@@ -2,42 +2,39 @@
 import re
 from typing import Iterator
 
-from liquid.expressions.common import Token
-from liquid.expressions.common import GROUP_QUOTED
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expressions.common import GROUP_IDENTINDEX
 from liquid.expressions.common import GROUP_IDENTQUOTED
+from liquid.expressions.common import GROUP_QUOTED
 from liquid.expressions.common import IDENTIFIER_PATTERN
-from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import IDENTINDEX_PATTERN
+from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import STRING_PATTERN
-
-from liquid.token import TOKEN_RANGE
-from liquid.token import TOKEN_FLOAT
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_TRUE
-from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_NIL
-from liquid.token import TOKEN_NULL
-from liquid.token import TOKEN_EMPTY
+from liquid.expressions.common import Token
 from liquid.token import TOKEN_BLANK
+from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_EMPTY
+from liquid.token import TOKEN_EQUALS
+from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_FLOAT
 from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_IDENTINDEX
+from liquid.token import TOKEN_IDENTSTRING
 from liquid.token import TOKEN_ILLEGAL
-from liquid.token import TOKEN_SKIP
-from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_INTEGER
 from liquid.token import TOKEN_LBRACKET
-from liquid.token import TOKEN_RBRACKET
 from liquid.token import TOKEN_LPAREN
+from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_NIL
+from liquid.token import TOKEN_NULL
+from liquid.token import TOKEN_RANGE
+from liquid.token import TOKEN_RBRACKET
 from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_COLON
-from liquid.token import TOKEN_COMMA
-from liquid.token import TOKEN_IDENTSTRING
-from liquid.token import TOKEN_IDENTINDEX
-from liquid.token import TOKEN_DOT
-from liquid.token import TOKEN_EQUALS
-
-from liquid.exceptions import LiquidSyntaxError
-
+from liquid.token import TOKEN_SKIP
+from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TRUE
 
 token_rules = (
     (TOKEN_IDENTINDEX, IDENTINDEX_PATTERN),
diff --git a/liquid/expressions/arguments/parse.py b/liquid/expressions/arguments/parse.py
index 32fe0856..adad0413 100644
--- a/liquid/expressions/arguments/parse.py
+++ b/liquid/expressions/arguments/parse.py
@@ -7,15 +7,12 @@
 from typing import Tuple
 
 from liquid.exceptions import LiquidSyntaxError
-
-from liquid.expression import Expression
 from liquid.expression import NIL
-
-from liquid.expressions.common import parse_unchained_identifier
+from liquid.expression import Expression
 from liquid.expressions.arguments.lex import tokenize
+from liquid.expressions.common import parse_unchained_identifier
 from liquid.expressions.filtered.parse import parse_obj
 from liquid.expressions.stream import TokenStream
-
 from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_EOF
@@ -23,7 +20,6 @@
 from liquid.token import TOKEN_IDENTIFIER
 from liquid.token import TOKEN_STRING
 
-
 Argument = Tuple[Any, Expression]
 
 
@@ -31,8 +27,7 @@ def make_parse_arguments(
     func: Callable[[TokenStream, str], Argument],
     separator_token: str = TOKEN_COLON,
 ) -> Callable[[TokenStream], List[Argument]]:
-    """Return an argument list parser using the given function to parse
-    each argument."""
+    """Return an argument list parser that uses _func_."""
 
     def _parse_arguments(stream: TokenStream) -> List[Argument]:
         """Parse arguments from a stream of tokens until EOF."""
@@ -80,7 +75,8 @@ def parse_keyword_arguments(expr: str, linenum: int = 1) -> Dict[str, Expression
 
     Values can be string, integer, float, true, false or nil literals, an
     identifier or a range expression. An identifier value could be chained
-    using a mixture of dot and bracket notation."""
+    using a mixture of dot and bracket notation.
+    """
     return dict(parse_colon_separated_arguments(TokenStream(tokenize(expr, linenum))))
 
 
diff --git a/liquid/expressions/boolean/__init__.py b/liquid/expressions/boolean/__init__.py
index 5d178442..56db2876 100644
--- a/liquid/expressions/boolean/__init__.py
+++ b/liquid/expressions/boolean/__init__.py
@@ -1,6 +1,4 @@
-# flake8: noqa
-# pylint: disable=missing-module-docstring
-from .lex import tokenize
+from .lex import tokenize  # noqa: D104
 from .lex import tokenize_with_parens
 from .parse import parse
 from .parse import parse_with_parens
diff --git a/liquid/expressions/boolean/lex.py b/liquid/expressions/boolean/lex.py
index ff5618bf..1f982b2f 100644
--- a/liquid/expressions/boolean/lex.py
+++ b/liquid/expressions/boolean/lex.py
@@ -1,46 +1,43 @@
 """Tokenize boolean liquid expressions."""
 import re
-
 from typing import Iterator
 
-from liquid.expressions.common import Token
-from liquid.expressions.common import GROUP_QUOTED
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expressions.common import GROUP_IDENTINDEX
 from liquid.expressions.common import GROUP_IDENTQUOTED
+from liquid.expressions.common import GROUP_QUOTED
 from liquid.expressions.common import IDENTIFIER_PATTERN
-from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import IDENTINDEX_PATTERN
+from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import STRING_PATTERN
-
-from liquid.token import operators
-from liquid.token import TOKEN_RANGE
-from liquid.token import TOKEN_RANGE_LITERAL
-from liquid.token import TOKEN_FLOAT
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_TRUE
-from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_NIL
-from liquid.token import TOKEN_NULL
-from liquid.token import TOKEN_EMPTY
+from liquid.expressions.common import Token
+from liquid.token import TOKEN_AND
 from liquid.token import TOKEN_BLANK
+from liquid.token import TOKEN_CONTAINS
+from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_EMPTY
+from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_FLOAT
 from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_IDENTINDEX
+from liquid.token import TOKEN_IDENTSTRING
 from liquid.token import TOKEN_ILLEGAL
-from liquid.token import TOKEN_SKIP
-from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_INTEGER
 from liquid.token import TOKEN_LBRACKET
-from liquid.token import TOKEN_RBRACKET
 from liquid.token import TOKEN_LPAREN
-from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_AND
-from liquid.token import TOKEN_OR
+from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_NIL
 from liquid.token import TOKEN_NOT
-from liquid.token import TOKEN_CONTAINS
-from liquid.token import TOKEN_IDENTSTRING
-from liquid.token import TOKEN_IDENTINDEX
-from liquid.token import TOKEN_DOT
-
-from liquid.exceptions import LiquidSyntaxError
+from liquid.token import TOKEN_NULL
+from liquid.token import TOKEN_OR
+from liquid.token import TOKEN_RANGE
+from liquid.token import TOKEN_RANGE_LITERAL
+from liquid.token import TOKEN_RBRACKET
+from liquid.token import TOKEN_RPAREN
+from liquid.token import TOKEN_SKIP
+from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TRUE
+from liquid.token import operators
 
 # Rules for the standard boolean expression.
 # Does not support grouping with parentheses.
@@ -144,8 +141,10 @@ def tokenize(source: str, linenum: int = 1) -> Iterator[Token]:
 
 
 def tokenize_with_parens(source: str, linenum: int = 1) -> Iterator[Token]:
-    """Yield tokens from a boolean expression with extra tokens intended to
-    distinguish the start of a range expression from the start of a logical group.
+    """Yield tokens from a boolean expression.
+
+    Extra tokens are generated to distinguish the start of a range expression from the
+    start of a logical group.
     """
     _keywords = not_keywords
     for match in PAREN_TOKENS_RE.finditer(source):
diff --git a/liquid/expressions/boolean/parse.py b/liquid/expressions/boolean/parse.py
index 6e52d45b..05d7023a 100644
--- a/liquid/expressions/boolean/parse.py
+++ b/liquid/expressions/boolean/parse.py
@@ -1,14 +1,14 @@
-"""Functions for parsing boolean expressions. Like those found in `if` and
-unless` tags.
-"""
-from typing import Dict
+"""Functions for parsing boolean expressions."""
 from typing import Callable
+from typing import Dict
 
-from liquid.expression import Expression
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expression import BooleanExpression
+from liquid.expression import Expression
 from liquid.expression import InfixExpression
 from liquid.expression import PrefixExpression
-
+from liquid.expressions.boolean.lex import tokenize
+from liquid.expressions.boolean.lex import tokenize_with_parens
 from liquid.expressions.common import make_parse_range
 from liquid.expressions.common import parse_blank
 from liquid.expressions.common import parse_boolean
@@ -18,42 +18,35 @@
 from liquid.expressions.common import parse_integer_literal
 from liquid.expressions.common import parse_nil
 from liquid.expressions.common import parse_string_literal
-
 from liquid.expressions.filtered.parse import parse_obj as parse_simple_obj
-from liquid.expressions.boolean.lex import tokenize
-from liquid.expressions.boolean.lex import tokenize_with_parens
 from liquid.expressions.stream import TokenStream
-
-from liquid.exceptions import LiquidSyntaxError
-
+from liquid.token import TOKEN_AND
 from liquid.token import TOKEN_BLANK
+from liquid.token import TOKEN_CONTAINS
 from liquid.token import TOKEN_EMPTY
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EQ
 from liquid.token import TOKEN_FALSE
 from liquid.token import TOKEN_FLOAT
+from liquid.token import TOKEN_GE
+from liquid.token import TOKEN_GT
 from liquid.token import TOKEN_IDENTIFIER
 from liquid.token import TOKEN_INTEGER
 from liquid.token import TOKEN_LBRACKET
+from liquid.token import TOKEN_LE
+from liquid.token import TOKEN_LG
 from liquid.token import TOKEN_LPAREN
+from liquid.token import TOKEN_LT
+from liquid.token import TOKEN_NE
 from liquid.token import TOKEN_NIL
+from liquid.token import TOKEN_NOT
 from liquid.token import TOKEN_NULL
+from liquid.token import TOKEN_OR
 from liquid.token import TOKEN_RANGE_LITERAL
 from liquid.token import TOKEN_RPAREN
 from liquid.token import TOKEN_STRING
 from liquid.token import TOKEN_TRUE
 
-from liquid.token import TOKEN_AND
-from liquid.token import TOKEN_CONTAINS
-from liquid.token import TOKEN_EQ
-from liquid.token import TOKEN_GE
-from liquid.token import TOKEN_GT
-from liquid.token import TOKEN_LE
-from liquid.token import TOKEN_LG
-from liquid.token import TOKEN_LT
-from liquid.token import TOKEN_NE
-from liquid.token import TOKEN_NOT
-from liquid.token import TOKEN_OR
-
 # Note that PREFIX and LOGICAL are not used in any "standard" expressions.
 PRECEDENCE_LOWEST = 1
 PRECEDENCE_LOGICALRIGHT = 3
@@ -136,13 +129,11 @@ def parse_infix_expression(stream: TokenStream, left: Expression) -> InfixExpres
     tok = stream.current
     precedence = PRECEDENCES.get(tok[1], PRECEDENCE_LOWEST)
     stream.next_token()
-
-    exp = InfixExpression(
+    return InfixExpression(
         left=left,
         operator=tok[2],
         right=parse_obj(stream, precedence),
     )
-    return exp
 
 
 def parse_infix_expression_with_parens(
@@ -271,8 +262,10 @@ def parse_obj_with_parens(
 
 
 def parse_with_parens(expr: str, linenum: int = 1) -> BooleanExpression:
-    """Parse a string as a boolean expression, possibly containing the logical `not`
-    operator and parentheses for grouping terms.
+    """Parse a string as a boolean expression.
+
+    This function handles expressions containing the logical `not` operator and
+    parentheses for grouping terms.
     """
     stream = TokenStream(tokenize_with_parens(expr, linenum))
     rv = BooleanExpression(parse_obj_with_parens(stream))
diff --git a/liquid/expressions/common.py b/liquid/expressions/common.py
index 812d6acd..276c7a2f 100644
--- a/liquid/expressions/common.py
+++ b/liquid/expressions/common.py
@@ -1,59 +1,57 @@
 """Patterns and parse functions common to multiple built-in expression types."""
 from __future__ import annotations
+
 import re
 import sys
-
+from typing import TYPE_CHECKING
 from typing import Callable
 from typing import Iterator
 from typing import Tuple
 from typing import Union
-from typing import TYPE_CHECKING
 
-from liquid.expression import Expression
-from liquid.expression import TRUE
-from liquid.expression import FALSE
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expression import BLANK
-from liquid.expression import NIL
 from liquid.expression import EMPTY
-from liquid.expression import Nil
+from liquid.expression import FALSE
+from liquid.expression import NIL
+from liquid.expression import TRUE
 from liquid.expression import Blank
-from liquid.expression import Empty
 from liquid.expression import Boolean
+from liquid.expression import Empty
+from liquid.expression import Expression
+from liquid.expression import FloatLiteral
 from liquid.expression import Identifier
 from liquid.expression import IdentifierPath
 from liquid.expression import IdentifierPathElement
-from liquid.expression import StringLiteral
 from liquid.expression import IntegerLiteral
-from liquid.expression import FloatLiteral
+from liquid.expression import Nil
 from liquid.expression import RangeLiteral
-
-from liquid.token import reverse_operators
-from liquid.token import TOKEN_TRUE
+from liquid.expression import StringLiteral
+from liquid.limits import to_int
+from liquid.token import TOKEN_BLANK
+from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_EMPTY
+from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_FLOAT
 from liquid.token import TOKEN_IDENTIFIER
 from liquid.token import TOKEN_IDENTINDEX
 from liquid.token import TOKEN_IDENTSTRING
-from liquid.token import TOKEN_DOT
-from liquid.token import TOKEN_LBRACKET
-from liquid.token import TOKEN_RBRACKET
+from liquid.token import TOKEN_ILLEGAL
 from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_FLOAT
+from liquid.token import TOKEN_LBRACKET
 from liquid.token import TOKEN_LPAREN
-from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_RANGE
-from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_NEWLINE
 from liquid.token import TOKEN_NIL
 from liquid.token import TOKEN_NULL
-from liquid.token import TOKEN_BLANK
-from liquid.token import TOKEN_EMPTY
-from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_NEWLINE
-from liquid.token import TOKEN_SKIP
-from liquid.token import TOKEN_ILLEGAL
-from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_OR
-
-from liquid.exceptions import LiquidSyntaxError
-from liquid.limits import to_int
+from liquid.token import TOKEN_RANGE
+from liquid.token import TOKEN_RBRACKET
+from liquid.token import TOKEN_RPAREN
+from liquid.token import TOKEN_SKIP
+from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TRUE
+from liquid.token import reverse_operators
 
 if TYPE_CHECKING:
     from liquid.expressions.stream import TokenStream
@@ -165,8 +163,9 @@ def parse_identifier(stream: "TokenStream") -> Identifier:
 def parse_string_or_identifier(
     stream: "TokenStream",
 ) -> Union[StringLiteral, Identifier]:
-    """Parse an expression from a stream of tokens. If the stream is not at a string or
-    identifier expression, raise a syntax error.
+    """Parse an expression from a stream of tokens.
+
+    If the stream is not at a string or identifier expression, raise a syntax error.
     """
     typ = stream.current[1]
     if typ in (TOKEN_IDENTIFIER, TOKEN_LBRACKET):
@@ -182,8 +181,11 @@ def parse_string_or_identifier(
 
 
 def parse_unchained_identifier(stream: "TokenStream") -> Identifier:
-    """Parse an identifier from a stream of tokens. If the stream is not at an
-    identifier or the identifier is chained, raise a syntax error."""
+    """Parse an identifier from a stream of tokens.
+
+    If the stream is not at an identifier or the identifier is chained, raise a syntax
+    error.
+    """
     tok = stream.current
     ident = parse_identifier(stream)
     if len(ident.path) != 1:
@@ -194,9 +196,7 @@ def parse_unchained_identifier(stream: "TokenStream") -> Identifier:
 def make_parse_range(
     parse_obj: Callable[["TokenStream"], Expression]
 ) -> Callable[["TokenStream"], RangeLiteral]:
-    """Return a function that parse range expressions using the given parse_obj
-    callable.
-    """
+    """Return a function that parses range expressions using _parse_obj_."""
 
     def _parse_range_literal(stream: "TokenStream") -> RangeLiteral:
         """Read a range literal from the token stream.
@@ -251,8 +251,11 @@ def _parse_range_literal(stream: "TokenStream") -> RangeLiteral:
 
 
 def _parse_common_identifier(stream: "TokenStream") -> Identifier:
-    """This is much like `parse_identifier`, but leaves the last ident
-    token on the stream."""
+    """Parse an identifier from a stream of tokens.
+
+    This is much like `parse_identifier`, but leaves the last ident
+    token on the stream.
+    """
     path: IdentifierPath = []
 
     while True:
diff --git a/liquid/expressions/conditional/__init__.py b/liquid/expressions/conditional/__init__.py
index 5d178442..56db2876 100644
--- a/liquid/expressions/conditional/__init__.py
+++ b/liquid/expressions/conditional/__init__.py
@@ -1,6 +1,4 @@
-# flake8: noqa
-# pylint: disable=missing-module-docstring
-from .lex import tokenize
+from .lex import tokenize  # noqa: D104
 from .lex import tokenize_with_parens
 from .parse import parse
 from .parse import parse_with_parens
diff --git a/liquid/expressions/conditional/lex.py b/liquid/expressions/conditional/lex.py
index 4a087ded..b1759d1d 100644
--- a/liquid/expressions/conditional/lex.py
+++ b/liquid/expressions/conditional/lex.py
@@ -2,51 +2,48 @@
 import re
 from typing import Iterator
 
-from liquid.expressions.common import Token
-from liquid.expressions.common import GROUP_QUOTED
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expressions.common import GROUP_IDENTINDEX
 from liquid.expressions.common import GROUP_IDENTQUOTED
+from liquid.expressions.common import GROUP_QUOTED
 from liquid.expressions.common import IDENTIFIER_PATTERN
-from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import IDENTINDEX_PATTERN
+from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import STRING_PATTERN
-
-from liquid.token import operators
-from liquid.token import TOKEN_RANGE
-from liquid.token import TOKEN_FLOAT
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_TRUE
-from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_NIL
-from liquid.token import TOKEN_NULL
-from liquid.token import TOKEN_EMPTY
+from liquid.expressions.common import Token
+from liquid.token import TOKEN_AND
 from liquid.token import TOKEN_BLANK
-from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_STRING
-from liquid.token import TOKEN_ILLEGAL
-from liquid.token import TOKEN_SKIP
-from liquid.token import TOKEN_NEWLINE
-from liquid.token import TOKEN_LBRACKET
-from liquid.token import TOKEN_RBRACKET
-from liquid.token import TOKEN_LPAREN
-from liquid.token import TOKEN_RPAREN
 from liquid.token import TOKEN_COLON
-from liquid.token import TOKEN_PIPE
-from liquid.token import TOKEN_DPIPE
 from liquid.token import TOKEN_COMMA
-from liquid.token import TOKEN_IDENTSTRING
-from liquid.token import TOKEN_IDENTINDEX
-from liquid.token import TOKEN_DOT
-from liquid.token import TOKEN_AND
-from liquid.token import TOKEN_OR
 from liquid.token import TOKEN_CONTAINS
+from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_DPIPE
 from liquid.token import TOKEN_ELSE
+from liquid.token import TOKEN_EMPTY
+from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_FLOAT
+from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_IDENTINDEX
+from liquid.token import TOKEN_IDENTSTRING
 from liquid.token import TOKEN_IF
+from liquid.token import TOKEN_ILLEGAL
+from liquid.token import TOKEN_INTEGER
+from liquid.token import TOKEN_LBRACKET
+from liquid.token import TOKEN_LPAREN
+from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_NIL
 from liquid.token import TOKEN_NOT
+from liquid.token import TOKEN_NULL
+from liquid.token import TOKEN_OR
+from liquid.token import TOKEN_PIPE
+from liquid.token import TOKEN_RANGE
 from liquid.token import TOKEN_RANGE_LITERAL
-
-from liquid.exceptions import LiquidSyntaxError
-
+from liquid.token import TOKEN_RBRACKET
+from liquid.token import TOKEN_RPAREN
+from liquid.token import TOKEN_SKIP
+from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TRUE
+from liquid.token import operators
 
 token_rules = (
     (TOKEN_IDENTINDEX, IDENTINDEX_PATTERN),
@@ -152,8 +149,10 @@ def tokenize(source: str, linenum: int = 1) -> Iterator[Token]:
 
 
 def tokenize_with_parens(source: str, linenum: int = 1) -> Iterator[Token]:
-    """Yield tokens from a conditional expression with extra tokens intended to
-    distinguish the start of a range expression from the start of a logical group.
+    """Yield tokens from a conditional expression.
+
+    This tokenizer includes extra tokens intended to distinguish the start of a range
+    expression from the start of a logical group.
     """
     _keywords = not_keywords
     for match in PAREN_TOKENS_RE.finditer(source):
diff --git a/liquid/expressions/conditional/parse.py b/liquid/expressions/conditional/parse.py
index 0b31c35f..2a712b4d 100644
--- a/liquid/expressions/conditional/parse.py
+++ b/liquid/expressions/conditional/parse.py
@@ -1,19 +1,18 @@
 """Functions for parsing non-standard conditional expressions."""
 from functools import partial
-
 from typing import Dict
-from typing import List
-from typing import Iterator
 from typing import Iterable
+from typing import Iterator
+from typing import List
 from typing import Optional
 from typing import Tuple
 
-from liquid.expression import Expression
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expression import FALSE
-from liquid.expression import Filter
 from liquid.expression import ConditionalExpression
+from liquid.expression import Expression
+from liquid.expression import Filter
 from liquid.expression import FilteredExpression
-
 from liquid.expressions.boolean.parse import parse_obj as parse_boolean_obj
 from liquid.expressions.boolean.parse import (
     parse_obj_with_parens as parse_boolean_obj_with_parens,
@@ -26,17 +25,14 @@
 )
 from liquid.expressions.filtered.parse import parse_obj
 from liquid.expressions.stream import TokenStream
-
-from liquid.exceptions import LiquidSyntaxError
-
-from liquid.token import TOKEN_EOF
-from liquid.token import TOKEN_PIPE
-from liquid.token import TOKEN_DPIPE
+from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_DPIPE
+from liquid.token import TOKEN_ELSE
+from liquid.token import TOKEN_EOF
 from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_IF
-from liquid.token import TOKEN_ELSE
+from liquid.token import TOKEN_PIPE
 
 
 def split_at_pipe(tokens: Iterable[Token]) -> Iterator[List[Token]]:
@@ -161,8 +157,10 @@ def parse(expr: str, linenum: int = 1) -> FilteredExpression:
 
 
 def parse_with_parens(expr: str, linenum: int = 1) -> FilteredExpression:
-    """Parse a conditional expression string that supports logical `not` and
-    grouping terms with parentheses."""
+    """Parse a conditional expression string.
+
+    This parse function handles logical `not` and grouping terms with parentheses.
+    """
     tokens = tokenize_with_parens(expr, linenum)
     standard_tokens, _conditional_tokens = split_at_first_if(tokens)
 
diff --git a/liquid/expressions/filtered/__init__.py b/liquid/expressions/filtered/__init__.py
index d4e11e64..12ada024 100644
--- a/liquid/expressions/filtered/__init__.py
+++ b/liquid/expressions/filtered/__init__.py
@@ -1,6 +1,4 @@
-# flake8: noqa
-# pylint: disable=missing-module-docstring
-from .lex import tokenize
+from .lex import tokenize  # noqa: D104
 from .parse import parse
 
 __all__ = (
diff --git a/liquid/expressions/filtered/lex.py b/liquid/expressions/filtered/lex.py
index 832bfa16..f4b067ee 100644
--- a/liquid/expressions/filtered/lex.py
+++ b/liquid/expressions/filtered/lex.py
@@ -6,42 +6,39 @@
 import re
 from typing import Iterator
 
-from liquid.expressions.common import Token
-from liquid.expressions.common import GROUP_QUOTED
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expressions.common import GROUP_IDENTINDEX
 from liquid.expressions.common import GROUP_IDENTQUOTED
+from liquid.expressions.common import GROUP_QUOTED
 from liquid.expressions.common import IDENTIFIER_PATTERN
-from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import IDENTINDEX_PATTERN
+from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import STRING_PATTERN
-
-from liquid.token import TOKEN_RANGE
-from liquid.token import TOKEN_FLOAT
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_TRUE
-from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_NIL
-from liquid.token import TOKEN_NULL
-from liquid.token import TOKEN_EMPTY
+from liquid.expressions.common import Token
 from liquid.token import TOKEN_BLANK
+from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_EMPTY
+from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_FLOAT
 from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_IDENTINDEX
+from liquid.token import TOKEN_IDENTSTRING
 from liquid.token import TOKEN_ILLEGAL
-from liquid.token import TOKEN_SKIP
-from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_INTEGER
 from liquid.token import TOKEN_LBRACKET
-from liquid.token import TOKEN_RBRACKET
 from liquid.token import TOKEN_LPAREN
-from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_NIL
+from liquid.token import TOKEN_NULL
 from liquid.token import TOKEN_PIPE
-from liquid.token import TOKEN_COMMA
-from liquid.token import TOKEN_IDENTSTRING
-from liquid.token import TOKEN_IDENTINDEX
-from liquid.token import TOKEN_DOT
-
-from liquid.exceptions import LiquidSyntaxError
-
+from liquid.token import TOKEN_RANGE
+from liquid.token import TOKEN_RBRACKET
+from liquid.token import TOKEN_RPAREN
+from liquid.token import TOKEN_SKIP
+from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TRUE
 
 token_rules = (
     (TOKEN_IDENTINDEX, IDENTINDEX_PATTERN),
diff --git a/liquid/expressions/filtered/parse.py b/liquid/expressions/filtered/parse.py
index 02c02a5c..aaeee547 100644
--- a/liquid/expressions/filtered/parse.py
+++ b/liquid/expressions/filtered/parse.py
@@ -1,19 +1,20 @@
-"""Functions for parsing filtered expressions. Like those found in output statements and
-assign tags.
+"""Functions for parsing filtered expressions.
+
+Like those found in output statements and assign tags.
 """
 from itertools import islice
-
 from typing import Dict
-from typing import List
-from typing import Iterator
 from typing import Iterable
+from typing import Iterator
+from typing import List
 from typing import Tuple
 
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expression import Expression
 from liquid.expression import Filter
 from liquid.expression import FilteredExpression
-
 from liquid.expressions.common import Token
+from liquid.expressions.common import make_parse_range
 from liquid.expressions.common import parse_blank
 from liquid.expressions.common import parse_boolean
 from liquid.expressions.common import parse_empty
@@ -22,13 +23,8 @@
 from liquid.expressions.common import parse_integer_literal
 from liquid.expressions.common import parse_nil
 from liquid.expressions.common import parse_string_literal
-from liquid.expressions.common import make_parse_range
-
 from liquid.expressions.filtered.lex import tokenize
 from liquid.expressions.stream import TokenStream
-
-from liquid.exceptions import LiquidSyntaxError
-
 from liquid.token import TOKEN_BLANK
 from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_COMMA
@@ -47,7 +43,6 @@
 from liquid.token import TOKEN_STRING
 from liquid.token import TOKEN_TRUE
 
-
 TOKEN_MAP = {
     TOKEN_FALSE: parse_boolean,
     TOKEN_TRUE: parse_boolean,
diff --git a/liquid/expressions/include/__init__.py b/liquid/expressions/include/__init__.py
index 7a948147..65c6ec28 100644
--- a/liquid/expressions/include/__init__.py
+++ b/liquid/expressions/include/__init__.py
@@ -1,5 +1,3 @@
-# flake8: noqa
-# pylint: disable=missing-module-docstring
-from .lex import tokenize
+from .lex import tokenize  # noqa: D104
 
 __all__ = ("tokenize",)
diff --git a/liquid/expressions/include/lex.py b/liquid/expressions/include/lex.py
index da3e4492..69c723bb 100644
--- a/liquid/expressions/include/lex.py
+++ b/liquid/expressions/include/lex.py
@@ -2,44 +2,41 @@
 import re
 from typing import Iterator
 
-from liquid.expressions.common import Token
-from liquid.expressions.common import GROUP_QUOTED
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expressions.common import GROUP_IDENTINDEX
 from liquid.expressions.common import GROUP_IDENTQUOTED
+from liquid.expressions.common import GROUP_QUOTED
 from liquid.expressions.common import IDENTIFIER_PATTERN
-from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import IDENTINDEX_PATTERN
+from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import STRING_PATTERN
-
-from liquid.token import TOKEN_RANGE
-from liquid.token import TOKEN_FLOAT
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_TRUE
-from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_NIL
-from liquid.token import TOKEN_NULL
-from liquid.token import TOKEN_EMPTY
+from liquid.expressions.common import Token
+from liquid.token import TOKEN_AS
 from liquid.token import TOKEN_BLANK
+from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_EMPTY
+from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_FLOAT
+from liquid.token import TOKEN_FOR
 from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_IDENTINDEX
+from liquid.token import TOKEN_IDENTSTRING
 from liquid.token import TOKEN_ILLEGAL
-from liquid.token import TOKEN_SKIP
-from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_INTEGER
 from liquid.token import TOKEN_LBRACKET
-from liquid.token import TOKEN_RBRACKET
 from liquid.token import TOKEN_LPAREN
+from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_NIL
+from liquid.token import TOKEN_NULL
+from liquid.token import TOKEN_RANGE
+from liquid.token import TOKEN_RBRACKET
 from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_COLON
-from liquid.token import TOKEN_COMMA
-from liquid.token import TOKEN_IDENTSTRING
-from liquid.token import TOKEN_IDENTINDEX
-from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_SKIP
+from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TRUE
 from liquid.token import TOKEN_WITH
-from liquid.token import TOKEN_FOR
-from liquid.token import TOKEN_AS
-
-from liquid.exceptions import LiquidSyntaxError
-
 
 token_rules = (
     (TOKEN_IDENTINDEX, IDENTINDEX_PATTERN),
diff --git a/liquid/expressions/loop/__init__.py b/liquid/expressions/loop/__init__.py
index d4e11e64..12ada024 100644
--- a/liquid/expressions/loop/__init__.py
+++ b/liquid/expressions/loop/__init__.py
@@ -1,6 +1,4 @@
-# flake8: noqa
-# pylint: disable=missing-module-docstring
-from .lex import tokenize
+from .lex import tokenize  # noqa: D104
 from .parse import parse
 
 __all__ = (
diff --git a/liquid/expressions/loop/lex.py b/liquid/expressions/loop/lex.py
index 514ed84c..4acc63c0 100644
--- a/liquid/expressions/loop/lex.py
+++ b/liquid/expressions/loop/lex.py
@@ -2,42 +2,39 @@
 import re
 from typing import Iterator
 
-from liquid.expressions.common import Token
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expressions.common import GROUP_IDENTINDEX
 from liquid.expressions.common import GROUP_IDENTQUOTED
 from liquid.expressions.common import GROUP_QUOTED
 from liquid.expressions.common import IDENTIFIER_PATTERN
-from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import IDENTINDEX_PATTERN
+from liquid.expressions.common import IDENTSTRING_PATTERN
 from liquid.expressions.common import STRING_PATTERN
-
-from liquid.token import TOKEN_RANGE
-from liquid.token import TOKEN_FLOAT
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_IN
-from liquid.token import TOKEN_OFFSET
-from liquid.token import TOKEN_LIMIT
-from liquid.token import TOKEN_REVERSED
+from liquid.expressions.common import Token
+from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_COLS
+from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_CONTINUE
+from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_FLOAT
 from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_IDENTINDEX
+from liquid.token import TOKEN_IDENTSTRING
 from liquid.token import TOKEN_ILLEGAL
-from liquid.token import TOKEN_SKIP
-from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_IN
+from liquid.token import TOKEN_INTEGER
 from liquid.token import TOKEN_LBRACKET
-from liquid.token import TOKEN_RBRACKET
+from liquid.token import TOKEN_LIMIT
 from liquid.token import TOKEN_LPAREN
-from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_NEWLINE
+from liquid.token import TOKEN_OFFSET
 from liquid.token import TOKEN_PIPE
-from liquid.token import TOKEN_IDENTSTRING
-from liquid.token import TOKEN_IDENTINDEX
-from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_RANGE
+from liquid.token import TOKEN_RBRACKET
+from liquid.token import TOKEN_REVERSED
+from liquid.token import TOKEN_RPAREN
+from liquid.token import TOKEN_SKIP
 from liquid.token import TOKEN_STRING
-from liquid.token import TOKEN_COMMA
-
-from liquid.exceptions import LiquidSyntaxError
-
 
 token_rules = (
     (TOKEN_IDENTINDEX, IDENTINDEX_PATTERN),
diff --git a/liquid/expressions/loop/parse.py b/liquid/expressions/loop/parse.py
index 8c459754..6227e15c 100644
--- a/liquid/expressions/loop/parse.py
+++ b/liquid/expressions/loop/parse.py
@@ -1,11 +1,27 @@
-"""Functions for parsing loop expressions. Like those found in `for` and `tablerow`
-tags."""
+"""Functions for parsing loop expressions.
+
+Like those found in `for` and `tablerow` tags.
+"""
 from typing import Callable
 from typing import Dict
 from typing import Tuple
 
+from liquid.exceptions import LiquidSyntaxError
+from liquid.expression import CONTINUE
+from liquid.expression import Continue
+from liquid.expression import IntegerLiteral
+from liquid.expression import LoopArgument
+from liquid.expression import LoopExpression
+from liquid.expression import LoopIterable
+from liquid.expressions.common import make_parse_range
+from liquid.expressions.common import parse_float_literal
+from liquid.expressions.common import parse_identifier
+from liquid.expressions.common import parse_integer_literal
+from liquid.expressions.common import parse_string_literal
+from liquid.expressions.filtered.parse import parse_obj as parse_simple_obj
+from liquid.expressions.loop.lex import tokenize
+from liquid.expressions.stream import TokenStream
 from liquid.limits import to_int
-
 from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_COLS
 from liquid.token import TOKEN_COMMA
@@ -22,25 +38,6 @@
 from liquid.token import TOKEN_REVERSED
 from liquid.token import TOKEN_STRING
 
-from liquid.expression import Continue
-from liquid.expression import CONTINUE
-from liquid.expression import IntegerLiteral
-from liquid.expression import LoopArgument
-from liquid.expression import LoopExpression
-from liquid.expression import LoopIterable
-
-from liquid.expressions.common import make_parse_range
-from liquid.expressions.common import parse_float_literal
-from liquid.expressions.common import parse_identifier
-from liquid.expressions.common import parse_integer_literal
-from liquid.expressions.common import parse_string_literal
-
-from liquid.expressions.filtered.parse import parse_obj as parse_simple_obj
-from liquid.expressions.stream import TokenStream
-from liquid.expressions.loop.lex import tokenize
-
-from liquid.exceptions import LiquidSyntaxError
-
 LOOP_ARGS = frozenset(
     (
         TOKEN_LIMIT,
@@ -86,8 +83,7 @@ def parse_loop_argument(stream: TokenStream) -> LoopArgument:
 
 
 def parse_loop_arguments(stream: TokenStream) -> Tuple[Dict[str, LoopArgument], bool]:
-    """Parse zero or more arguments from the stream of tokens until the end of the
-    stream."""
+    """Parse loop arguments from the stream of tokens until the end of the stream."""
     arguments: Dict[str, LoopArgument] = {}
     _reversed = False
 
diff --git a/liquid/expressions/stream.py b/liquid/expressions/stream.py
index 203d9abb..a36a8526 100644
--- a/liquid/expressions/stream.py
+++ b/liquid/expressions/stream.py
@@ -3,16 +3,17 @@
 from __future__ import annotations
 
 from collections import deque
-
+from typing import TYPE_CHECKING
 from typing import Deque
 from typing import Iterator
 
-from liquid.token import TOKEN_INITIAL
+from liquid.exceptions import LiquidSyntaxError
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_INITIAL
 from liquid.token import reverse_operators
 
-from liquid.expressions.common import Token
-from liquid.exceptions import LiquidSyntaxError
+if TYPE_CHECKING:
+    from liquid.expressions.common import Token
 
 
 class TokenStream:
@@ -38,7 +39,7 @@ def __next__(self) -> Token:
             tok = self.stream.current
             if tok[1] is TOKEN_EOF:
                 self.stream.close()
-                raise StopIteration()
+                raise StopIteration
             next(self.stream)
             return tok
 
@@ -85,8 +86,7 @@ def close(self) -> None:
         self.current = (0, TOKEN_EOF, "")
 
     def expect(self, typ: str) -> None:
-        """Raise an exception if the current token in the stream does not match the
-        given type."""
+        """Raise a `LiquidSyntaxError` if the current token type doesn't match `typ`."""
         if self.current[1] != typ:
             raise LiquidSyntaxError(
                 f"expected {reverse_operators.get(typ, typ)!r}, "
@@ -95,8 +95,7 @@ def expect(self, typ: str) -> None:
             )
 
     def expect_peek(self, typ: str) -> None:
-        """Raise an exception if the next token in the stream does not match the given
-        type."""
+        """Raise a `LiquidSyntaxError` if the next token type does not match `typ`."""
         if self.peek[1] != typ:
             raise LiquidSyntaxError(
                 f"expected {reverse_operators.get(typ, typ)!r}, "
diff --git a/liquid/extra/__init__.py b/liquid/extra/__init__.py
index 10053d4f..ef4ac3c3 100644
--- a/liquid/extra/__init__.py
+++ b/liquid/extra/__init__.py
@@ -1,13 +1,12 @@
-# pylint: disable=missing-module-docstring
-from __future__ import annotations
+from __future__ import annotations  # noqa: D104
+
 from typing import TYPE_CHECKING
 
-from .filters import index
 from .filters import JSON
+from .filters import index
 from .filters import script_tag
 from .filters import sort_numeric
 from .filters import stylesheet_tag
-
 from .tags import BlockTag
 from .tags import CallTag
 from .tags import ExtendsTag
@@ -21,8 +20,7 @@
 from .tags import MacroTag
 from .tags import WithTag
 
-
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
 
 __all__ = (
@@ -53,18 +51,24 @@
 
 
 def add_inline_expression_tags(env: Environment) -> None:
-    """Replace standard implementations of the output statement,
+    """Add extra inline expression tags to an environment.
+
+    Replace standard implementations of the output statement,
     `echo` tag and `assign` tag with ones that support inline `if`
-    expressions."""
+    expressions.
+    """
     env.add_tag(InlineIfAssignTag)
     env.add_tag(InlineIfEchoTag)
     env.add_tag(InlineIfStatement)
 
 
 def add_extended_inline_expression_tags(env: Environment) -> None:
-    """Replace standard implementations of the output statement,
+    """Add extra extended inline expressions tags to an environment.
+
+    Replace standard implementations of the output statement,
     `echo` tag and `assign` tag with ones that support inline `if`
-    expressions."""
+    expressions.
+    """
     env.add_tag(InlineIfAssignTagWithParens)
     env.add_tag(InlineIfEchoTagWithParens)
     env.add_tag(InlineIfStatementWithParens)
@@ -92,8 +96,7 @@ def add_tags(env: Environment) -> None:  # pragma: no cover
 
 
 def add_filters(env: Environment) -> None:
-    """Register all extra filters with an environment with their default
-    and options."""
+    """Register all extra filters with an environment with their default options."""
     env.add_filter("index", index)
     env.add_filter("json", JSON())
     env.add_filter("script_tag", script_tag)
diff --git a/liquid/extra/filters/__init__.py b/liquid/extra/filters/__init__.py
index 43d321a2..7084d816 100644
--- a/liquid/extra/filters/__init__.py
+++ b/liquid/extra/filters/__init__.py
@@ -1,10 +1,8 @@
-# pylint: disable=missing-module-docstring
+from ._json import JSON  # noqa: D104
 from .array import index
 from .array import sort_numeric
 from .html import script_tag
 from .html import stylesheet_tag
-from ._json import JSON
-
 
 __all__ = (
     "index",
diff --git a/liquid/extra/filters/_json.py b/liquid/extra/filters/_json.py
index 6949d631..6b499d20 100644
--- a/liquid/extra/filters/_json.py
+++ b/liquid/extra/filters/_json.py
@@ -1,6 +1,5 @@
 """A JSON filter."""
 import json
-
 from typing import Any
 from typing import Callable
 from typing import Optional
@@ -9,16 +8,15 @@
 from liquid.filter import liquid_filter
 
 
-class JSON:  # pylint: disable=too-few-public-methods
+class JSON:
     """Serialize an object to a JSON formatted string.
 
-    :param default: A function passed to `json.dumps`. This function is called
-        in the event that the JSONEncoder does not know how to serialize an
-        object. Defaults to ``None``.
-    :type default: Optional[Callable[[Any], Any]]
-    :param strip_tags: If ``True``, HTML tags will be removed from the resulting
-        JSON formatted string. Defaults to ``True``.
-    :type strip_tags: bool
+    Args:
+        default: A function passed to `json.dumps`. This function is called
+            in the event that the JSONEncoder does not know how to serialize an
+            object. Defaults to `None`.
+        strip_tags: If `True`, HTML tags will be removed from the resulting
+            JSON formatted string. Defaults to `True`.
     """
 
     name = "json"
diff --git a/liquid/extra/filters/array.py b/liquid/extra/filters/array.py
index 6e7ddfa6..245086bb 100644
--- a/liquid/extra/filters/array.py
+++ b/liquid/extra/filters/array.py
@@ -1,10 +1,8 @@
 """Extra array filters."""
 import math
 import re
-
 from decimal import Decimal
 from operator import getitem
-
 from typing import Any
 from typing import List
 from typing import Sequence
@@ -13,14 +11,14 @@
 
 from liquid.filter import array_filter
 from liquid.filter import sequence_filter
-
 from liquid.limits import to_int
 
 
 @array_filter
 def index(left: Sequence[object], obj: object) -> object:
-    """Return the zero-based index of an item in an array, or None if
-    the items is not in the array.
+    """Return the zero-based index of an array item.
+
+    `None` is returned if `obj` is not in `left`.
     """
     try:
         return left.index(obj)
@@ -33,8 +31,7 @@ def index(left: Sequence[object], obj: object) -> object:
 
 @sequence_filter
 def sort_numeric(left: Sequence[object], key: object = None) -> List[object]:
-    """Return a copy of the input sequence sorted by any numeric values found in
-    the sequence's items."""
+    """Return a copy of `left` sorted by numeric values found in `left`'s items."""
     if key:
         _key = str(key)
         return sorted(left, key=lambda item: _ints(_getitem(item, _key)))
@@ -42,7 +39,7 @@ def sort_numeric(left: Sequence[object], key: object = None) -> List[object]:
 
 
 def _getitem(sequence: Any, key: object, default: object = None) -> Any:
-    """Item getter for the ``sort_numeric`` filter."""
+    """Item getter for the `sort_numeric` filter."""
     try:
         return getitem(sequence, key)
     except (KeyError, IndexError, TypeError):
@@ -50,7 +47,7 @@ def _getitem(sequence: Any, key: object, default: object = None) -> Any:
 
 
 def _ints(obj: object) -> Tuple[Union[int, float, Decimal], ...]:
-    """Key function for the ``sort_numeric`` filter."""
+    """Key function for the `sort_numeric` filter."""
     if isinstance(obj, bool):
         # isinstance(False, int) == True
         return (math.inf,)
diff --git a/liquid/extra/filters/html.py b/liquid/extra/filters/html.py
index 57d8e2d1..ab4ba3b0 100644
--- a/liquid/extra/filters/html.py
+++ b/liquid/extra/filters/html.py
@@ -1,14 +1,14 @@
 """Extra HTML filters."""
 from __future__ import annotations
-import html
 
+import html
 from typing import TYPE_CHECKING
 
 from liquid import Markup
 from liquid.filter import string_filter
 from liquid.filter import with_environment
 
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
 
 
diff --git a/liquid/extra/tags/__init__.py b/liquid/extra/tags/__init__.py
index 518cfbaa..3e3c8ab8 100644
--- a/liquid/extra/tags/__init__.py
+++ b/liquid/extra/tags/__init__.py
@@ -1,5 +1,4 @@
-# pylint: disable=missing-module-docstring
-from ._with import WithTag
+from ._with import WithTag  # noqa: D104
 from .extends import BlockTag
 from .extends import ExtendsTag
 from .if_expressions import InlineIfAssignTag
diff --git a/liquid/extra/tags/_with.py b/liquid/extra/tags/_with.py
index fca7f3e1..1197341b 100644
--- a/liquid/extra/tags/_with.py
+++ b/liquid/extra/tags/_with.py
@@ -1,9 +1,7 @@
 """Node and tag definitions for `with`."""
-# pylint: disable=missing-class-docstring
 from __future__ import annotations
 
 import sys
-
 from typing import TYPE_CHECKING
 from typing import Dict
 from typing import List
@@ -11,28 +9,23 @@
 from typing import Optional
 from typing import TextIO
 
+from liquid.ast import BlockNode
 from liquid.ast import ChildNode
 from liquid.ast import Node
-from liquid.ast import BlockNode
-
-from liquid.context import Context
-from liquid.expression import Expression
 from liquid.expressions import parse_keyword_arguments
-
 from liquid.parse import expect
 from liquid.parse import get_parser
-
-from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_EXPRESSION
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
-
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
+    from liquid.context import Context
+    from liquid.expression import Expression
+    from liquid.stream import TokenStream
 
 TAG_WITH = sys.intern("with")
 TAG_ENDWITH = sys.intern("endwith")
diff --git a/liquid/extra/tags/extends.py b/liquid/extra/tags/extends.py
index 16859c4b..0dede052 100644
--- a/liquid/extra/tags/extends.py
+++ b/liquid/extra/tags/extends.py
@@ -1,11 +1,10 @@
 """Template inheritance tags."""
-# pylint: disable=missing-class-docstring
 from __future__ import annotations
-import sys
 
+import sys
 from collections import defaultdict
 from dataclasses import dataclass
-
+from typing import TYPE_CHECKING
 from typing import DefaultDict
 from typing import Iterator
 from typing import List
@@ -15,44 +14,37 @@
 from typing import Set
 from typing import TextIO
 from typing import Tuple
-from typing import TYPE_CHECKING
 
 from liquid import Markup
-
+from liquid.ast import BlockNode as TemplateBlockNode
 from liquid.ast import ChildNode
 from liquid.ast import Node
-from liquid.ast import BlockNode as TemplateBlockNode
-
 from liquid.exceptions import LiquidEnvironmentError
 from liquid.exceptions import LiquidSyntaxError
 from liquid.exceptions import RequiredBlockError
 from liquid.exceptions import StopRender
 from liquid.exceptions import TemplateInheritanceError
-
 from liquid.expression import StringLiteral
-
 from liquid.expressions import TokenStream as ExprTokenStream
 from liquid.expressions.common import parse_string_literal
 from liquid.expressions.common import tokenize_common_expression
-
 from liquid.parse import expect
 from liquid.parse import get_parser
-
-from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_EXPRESSION
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
 from liquid.token import TOKEN_IDENTIFIER
 from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
-if TYPE_CHECKING:  # pragma: no cover
-    from liquid import Environment
+if TYPE_CHECKING:
     from liquid import BoundTemplate
+    from liquid import Environment
     from liquid.context import Context
+    from liquid.stream import TokenStream
 
+# ruff: noqa: D102
 
 TAG_EXTENDS = sys.intern("extends")
 TAG_BLOCK = sys.intern("block")
@@ -118,6 +110,8 @@ class _BlockStackItem:
 
 
 class BlockNode(Node):
+    """The extra "Block" node."""
+
     __slots__ = ("tok", "name", "block", "expr", "required")
 
     def __init__(
@@ -204,6 +198,8 @@ def children(self) -> List[ChildNode]:
 
 
 class ExtendsNode(Node):
+    """The extra "Extends" node."""
+
     __slots__ = ("tok", "name")
     tag = TAG_EXTENDS
 
@@ -228,7 +224,7 @@ def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
 
         base_template.render_with_context(context, buffer)
         context.tag_namespace["extends"].clear()
-        raise StopRender()
+        raise StopRender
 
     async def render_to_output_async(
         self, context: Context, buffer: TextIO
@@ -249,7 +245,7 @@ async def render_to_output_async(
 
         await base_template.render_with_context_async(context, buffer)
         context.tag_namespace["extends"].clear()
-        raise StopRender()
+        raise StopRender
 
     def children(self) -> List[ChildNode]:
         return [
@@ -263,6 +259,8 @@ def children(self) -> List[ChildNode]:
 
 
 class BlockTag(Tag):
+    """The extra "Block" tag."""
+
     name = TAG_BLOCK
     end = TAG_ENDBLOCK
 
@@ -322,6 +320,8 @@ def _parse_block_name(self, stream: ExprTokenStream) -> StringLiteral:
 
 
 class ExtendsTag(Tag):
+    """The extra "Extends" tag."""
+
     name = TAG_EXTENDS
     block = False
 
@@ -345,14 +345,15 @@ def build_block_stacks(
     parent_name: str,
     tag: str = TAG_EXTENDS,
 ) -> BoundTemplate:
-    """Build a stack for each `{% block %}` (one stack per block name) in the
-    inheritance chain. Blocks defined in the base template will be at the top of the
-    stack.
-
-    :param context: A render context to build the block stacks in.
-    :param template: A leaf template with an `extends` tag.
-    :param parent_name: The name of the immediate parent template as a string.
-    :param tag: The name of the `extends` tag, if it is overridden.
+    """Build a stack for each `{% block %}` in the inheritance chain.
+
+    Blocks defined in the base template will be at the top of the stack.
+
+    Args:
+        context: A render context to build the block stacks in.
+        template: A leaf template with an `extends` tag.
+        parent_name: The name of the immediate parent template as a string.
+        tag: The name of the `extends` tag, if it is overridden.
     """
     if "extends" not in context.tag_namespace:
         context.tag_namespace["extends"] = defaultdict(list)
@@ -440,8 +441,10 @@ def _visit_node(
 def stack_blocks(
     context: Context, template: BoundTemplate
 ) -> Tuple[Optional[ExtendsNode], List[BlockNode]]:
-    """Find template inheritance nodes in the given template and add them to the
-    context's block stacks."""
+    """Find template inheritance nodes in `template`.
+
+    Each node found is pushed on to the appropriate block stack.
+    """
     extends, blocks = find_inheritance_nodes(template)
     template_name = str(template.path or template.name)
 
@@ -475,11 +478,7 @@ def _store_blocks(context: Context, blocks: List[BlockNode], source_name: str) -
 
     for block in blocks:
         stack = block_stacks[block.name]
-
-        if stack and not block.required:
-            required = False
-        else:
-            required = block.required
+        required = False if stack and not block.required else block.required
 
         stack.append(
             _BlockStackItem(
diff --git a/liquid/extra/tags/if_expressions.py b/liquid/extra/tags/if_expressions.py
index e41e26e0..650d633a 100644
--- a/liquid/extra/tags/if_expressions.py
+++ b/liquid/extra/tags/if_expressions.py
@@ -1,21 +1,26 @@
-"""Drop-in replacements for the standard output statement, `assign` tag
+"""If expression nodes and tags.
+
+Drop-in replacements for the standard output statement, `assign` tag
 and `echo` tag that support inline `if` expressions, optionally including
-the logical `not` operator and grouping terms with parentheses."""
+the logical `not` operator and grouping terms with parentheses.
+"""
 
+from liquid.builtin.statement import Statement
+from liquid.builtin.statement import StatementNode
+from liquid.builtin.tags.assign_tag import AssignTag
+from liquid.builtin.tags.echo_tag import EchoTag
 from liquid.expression import Expression
 from liquid.parse import expect
 from liquid.stream import TokenStream
 from liquid.token import TOKEN_STATEMENT
 
-from liquid.builtin.tags.assign_tag import AssignTag
-from liquid.builtin.tags.echo_tag import EchoTag
-from liquid.builtin.statement import Statement
-from liquid.builtin.statement import StatementNode
+# ruff: noqa: D102 D205
 
 
 class InlineIfStatement(Statement):
     """A drop-in replacement for the standard output statement that supports
-    inline ``if`` expressions."""
+    inline `if` expressions.
+    """
 
     def parse(self, stream: TokenStream) -> StatementNode:
         tok = stream.current
@@ -26,16 +31,18 @@ def parse(self, stream: TokenStream) -> StatementNode:
 
 
 class InlineIfAssignTag(AssignTag):
-    """A drop-in replacement for the standard ``assign`` tag that supports
-    inline ``if`` expressions."""
+    """A drop-in replacement for the standard `assign` tag that supports
+    inline `if` expressions.
+    """
 
     def _parse_expression(self, value: str) -> Expression:
         return self.env.parse_conditional_expression_value(value)
 
 
 class InlineIfEchoTag(EchoTag):
-    """A drop-in replacement for the standard ``echo`` tag that supports
-    inline ``if`` expressions."""
+    """A drop-in replacement for the standard `echo` tag that supports
+    inline `if` expressions.
+    """
 
     def _parse_expression(self, value: str) -> Expression:
         return self.env.parse_conditional_expression_value(value)
@@ -43,8 +50,9 @@ def _parse_expression(self, value: str) -> Expression:
 
 class InlineIfStatementWithParens(Statement):
     """A drop-in replacement for the standard output statement that supports
-    inline ``if`` expressions with the logical ``not`` operator and grouping
-    terms with parentheses."""
+    inline `if` expressions with the logical `not` operator and grouping
+    terms with parentheses.
+    """
 
     def parse(self, stream: TokenStream) -> StatementNode:
         tok = stream.current
@@ -55,18 +63,20 @@ def parse(self, stream: TokenStream) -> StatementNode:
 
 
 class InlineIfAssignTagWithParens(AssignTag):
-    """A drop-in replacement for the standard ``assign`` tag that supports
-    inline ``if`` expressions with the logical ``not`` operator and grouping
-    terms with parentheses."""
+    """A drop-in replacement for the standard `assign` tag that supports
+    inline `if` expressions with the logical `not` operator and grouping
+    terms with parentheses.
+    """
 
     def _parse_expression(self, value: str) -> Expression:
         return self.env.parse_conditional_expression_value_with_parens(value)
 
 
 class InlineIfEchoTagWithParens(EchoTag):
-    """A drop-in replacement for the standard ``echo`` tag that supports
-    inline ``if`` expressions with the logical ``not`` operator and grouping
-    terms with parentheses."""
+    """A drop-in replacement for the standard `echo` tag that supports
+    inline `if` expressions with the logical `not` operator and grouping
+    terms with parentheses.
+    """
 
     def _parse_expression(self, value: str) -> Expression:
         return self.env.parse_conditional_expression_value_with_parens(value)
diff --git a/liquid/extra/tags/if_not.py b/liquid/extra/tags/if_not.py
index 4df544c1..821db7ce 100644
--- a/liquid/extra/tags/if_not.py
+++ b/liquid/extra/tags/if_not.py
@@ -1,5 +1,8 @@
-"""A drop-in replacement for the standard `if` tag that supports the
-logical `not` operator and grouping terms with parentheses."""
+"""An "if" tag that supports `not`.
+
+A drop-in replacement for the standard `if` tag that supports the
+logical `not` operator and grouping terms with parentheses.
+"""
 
 from liquid.builtin.tags.if_tag import IfTag
 from liquid.expression import Expression
@@ -11,7 +14,8 @@
 
 class IfNotTag(IfTag):
     """A drop-in replacement for the standard `if` tag that supports the
-    logical `not` operator and grouping terms with parentheses."""
+    logical `not` operator and grouping terms with parentheses.
+    """  # noqa: D205
 
     def parse_expression(self, stream: TokenStream) -> Expression:
         """Pare a boolean expression from a stream of tokens."""
diff --git a/liquid/extra/tags/macro.py b/liquid/extra/tags/macro.py
index fda91621..1f860090 100644
--- a/liquid/extra/tags/macro.py
+++ b/liquid/extra/tags/macro.py
@@ -3,49 +3,41 @@
 
 import itertools
 import sys
-
-from typing import Optional
+from typing import TYPE_CHECKING
 from typing import Dict
 from typing import List
 from typing import NamedTuple
+from typing import Optional
 from typing import TextIO
 from typing import Union
-from typing import TYPE_CHECKING
 
+from liquid.ast import BlockNode
 from liquid.ast import ChildNode
 from liquid.ast import Node
-from liquid.ast import BlockNode
-
-from liquid.context import is_undefined
-from liquid.context import Undefined
+from liquid.builtin.tags.include_tag import TAG_INCLUDE
 from liquid.context import Context
 from liquid.context import ReadOnlyChainMap
-
-from liquid.expression import Expression
+from liquid.context import Undefined
+from liquid.context import is_undefined
 from liquid.expression import NIL
-
+from liquid.expression import Expression
 from liquid.expressions import parse_call_arguments
 from liquid.expressions import parse_macro_arguments
-from liquid.expressions.arguments.parse import Argument
-
+from liquid.extra.tags.extends import TAG_BLOCK
 from liquid.parse import expect
 from liquid.parse import get_parser
-
-from liquid.stream import TokenStream
 from liquid.tag import Tag
-
-from liquid.builtin.tags.include_tag import TAG_INCLUDE
-from liquid.extra.tags.extends import TAG_BLOCK
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_EXPRESSION
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
-
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
+    from liquid.expressions.arguments.parse import Argument
+    from liquid.stream import TokenStream
 
+# ruff: noqa: D102
 
 TAG_MACRO = sys.intern("macro")
 TAG_ENDMACRO = sys.intern("endmacro")
@@ -96,7 +88,7 @@ def __str__(self) -> str:  # pragma: no cover
     def __repr__(self) -> str:  # pragma: no cover
         return f"MacroNode(tok={self.tok}, name={self.name}, block='{self.block}')"
 
-    def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
+    def render_to_output(self, context: Context, _: TextIO) -> Optional[bool]:
         if "macros" not in context.tag_namespace:
             context.tag_namespace["macros"] = {}
 
diff --git a/liquid/filter.py b/liquid/filter.py
index 3e975b8b..cd5bc6d1 100644
--- a/liquid/filter.py
+++ b/liquid/filter.py
@@ -3,53 +3,53 @@
 from __future__ import annotations
 
 from functools import wraps
-
+from typing import TYPE_CHECKING
 from typing import Any
 from typing import Callable
 from typing import Iterable
 from typing import Iterator
 from typing import List
-from typing import Union
 from typing import Optional
-from typing import TYPE_CHECKING
+from typing import Union
 
 from liquid.context import Undefined
-
 from liquid.exceptions import FilterArgumentError
 from liquid.exceptions import FilterValueError
-
 from liquid.limits import to_int
 
-if TYPE_CHECKING:  # pragma: no cover
-    FilterT = Callable[..., Any]  # pylint: disable=invalid-name
-    NumberT = Union[float, int]  # pylint: disable=invalid-name
+if TYPE_CHECKING:
+    FilterT = Callable[..., Any]
+    NumberT = Union[float, int]
 
 
 def with_context(_filter: FilterT) -> FilterT:
-    """Pass the active :class:`liquid.context.Context` as the named argument
-    ``context`` to the decorated filter function.
+    """Pass the active render context to decorated filter functions.
+
+    If a function is decorated with `with_context`, that function should
+    accept a `context` keyword argument, being the active render context.
 
-    :param _filter: The filter function to decorate.
-    :type _filter: Callable[..., Any]
+    Args:
+        _filter: The filter function to decorate.
     """
     _filter.with_context = True  # type: ignore
     return _filter
 
 
 def with_environment(_filter: FilterT) -> FilterT:
-    """Pass the active :class:`liquid.Environment` as the named argument
-    ``environment`` to the decorated filter function.
+    """Pass the active environment to decorated filter functions.
+
+    If a function is decorated with `with_environment`, that function should
+    accept an `environment` keyword argument, being the active environment.
 
-    :param _filter: The filter function to decorate.
-    :type _filter: Callable[..., Any]
+    Args:
+        _filter: The filter function to decorate.
     """
     _filter.with_environment = True  # type: ignore
     return _filter
 
 
 def string_filter(_filter: FilterT) -> FilterT:
-    """A filter function decorator that converts the first positional argument to a
-    string."""
+    """A filter function decorator that converts the first argument to a string."""
 
     @wraps(_filter)
     def wrapper(val: object, *args: Any, **kwargs: Any) -> Any:
@@ -64,8 +64,7 @@ def wrapper(val: object, *args: Any, **kwargs: Any) -> Any:
 
 
 def array_filter(_filter: FilterT) -> FilterT:
-    """A filter function decorator that raises a FilterValueError if the filter value
-    is not array-like."""
+    """Raise a `FilterValueError` if the left value is not array-like."""
 
     @wraps(_filter)
     def wrapper(val: object, *args: Any, **kwargs: Any) -> Any:
@@ -81,11 +80,10 @@ def wrapper(val: object, *args: Any, **kwargs: Any) -> Any:
 
 
 def sequence_filter(_filter: FilterT) -> FilterT:
-    """A filter function decorator that raises a FilterValueError if the filter value
-    can not be coerced into an array-like object.
+    """Coerce the left value to sequence.
 
     This is intended to mimic the semantics of the reference implementation's
-    ``InputIterator`` class.
+    `InputIterator` class.
     """
 
     @wraps(_filter)
@@ -117,8 +115,7 @@ def wrapper(val: object, *args: Any, **kwargs: Any) -> Any:
 
 
 def int_arg(val: Any, default: Optional[int] = None) -> int:
-    """Return the ``val`` as an int or ``default`` if ``val`` can't be cast to an
-    int."""
+    """Return `val` as an int or `default` if `val` can't be cast to an int."""
     try:
         return to_int(val)
     except ValueError as err:
@@ -130,8 +127,10 @@ def int_arg(val: Any, default: Optional[int] = None) -> int:
 
 
 def num_arg(val: Any, default: Optional[NumberT] = None) -> NumberT:
-    """Return the ``val`` as an int or float. If ``val`` can't be cast to an
-    int or float, return ``default`."""
+    """Return `val` as an int or float.
+
+    If `val` can't be cast to an int or float, return `default`.
+    """
     if isinstance(val, (int, float)):
         return val
 
@@ -159,8 +158,7 @@ def num_arg(val: Any, default: Optional[NumberT] = None) -> NumberT:
 
 
 def math_filter(_filter: FilterT) -> FilterT:
-    """A filter function decorator that raises a FilterArgumentError if the filter value
-    is not a number."""
+    """Raise a `FilterArgumentError` if the filter value can not be a number."""
 
     @wraps(_filter)
     def wrapper(val: object, *args: Any, **kwargs: Any) -> Any:
diff --git a/liquid/future/__init__.py b/liquid/future/__init__.py
index 41732ea9..f0c32ef7 100644
--- a/liquid/future/__init__.py
+++ b/liquid/future/__init__.py
@@ -1,4 +1,3 @@
-# pylint: disable=missing-module-docstring
-from .environment import Environment
+from .environment import Environment  # noqa: D104
 
 __all__ = ("Environment",)
diff --git a/liquid/future/environment.py b/liquid/future/environment.py
index 5835b5ed..efd66907 100644
--- a/liquid/future/environment.py
+++ b/liquid/future/environment.py
@@ -1,5 +1,8 @@
-"""An environment that is configured for maximum compatibility with Ruby Liquid, without
-concern for backwards incompatible changes to template rendering behavior."""
+"""An environment that is configured for maximum compatibility with Ruby Liquid.
+
+This environment will be updated without concern for backwards incompatible changes to
+template rendering behavior.
+"""
 from ..environment import Environment as DefaultEnvironment
 from ..template import FutureBoundTemplate
 
@@ -8,9 +11,9 @@ class Environment(DefaultEnvironment):
     """An environment that is configured for maximum compatibility with Ruby Liquid.
 
     This environment addresses some compatibility issues between Python Liquid's default
-    :class:`Environment` and Ruby Liquid. These issues are considered to be an
-    unacceptable breaking changes for users that rely on existing behavior of the
-    default environment.
+    `Environment` and Ruby Liquid. These issues are considered to be an unacceptable
+    breaking changes for users that rely on existing behavior of the default
+    environment.
 
     See https://jg-rp.github.io/liquid/known_issues
     """
diff --git a/liquid/golden/__init__.py b/liquid/golden/__init__.py
index 6dde1f22..c79ab1a5 100644
--- a/liquid/golden/__init__.py
+++ b/liquid/golden/__init__.py
@@ -1,87 +1,82 @@
-# flake8: noqa
-# pylint: disable=useless-import-alias,missing-module-docstring
-
+from . import abs_filter  # noqa: D104
+from . import append_filter
 from . import assign_tag
+from . import at_least_filter
+from . import at_most_filter
+from . import base64_decode_filter
+from . import base64_encode_filter
+from . import base64_url_safe_decode_filter
+from . import base64_url_safe_encode_filter
+from . import capitalize_filter
 from . import capture_tag
 from . import case_tag
+from . import ceil_filter
 from . import comment_tag
+from . import compact_filter
+from . import concat_filter
 from . import cycle_tag
+from . import date_filter
 from . import decrement_tag
+from . import default_filter
+from . import divided_by_filter
+from . import downcase_filter
 from . import echo_tag
+from . import escape_filter
+from . import escape_once_filter
+from . import first_filter
+from . import floor_filter
 from . import for_tag
+from . import identifiers
 from . import if_tag
 from . import ifchanged_tag
 from . import illegal
 from . import include_tag
 from . import increment_tag
-from . import liquid_tag
-from . import not_liquid
-from . import output_statement
-from . import raw_tag
-from . import render_tag
-from . import tablerow_tag
-from . import unless_tag
-from . import whitespace_control
 from . import inline_comment_tag
-
-from . import concat_filter
-from . import first_filter
 from . import join_filter
 from . import last_filter
+from . import liquid_tag
+from . import lstrip_filter
 from . import map_filter
-from . import reverse_filter
-from . import sort_filter
-from . import sort_natural_filter
-from . import where_filter
-from . import uniq_filter
-from . import compact_filter
-from . import abs_filter
-from . import at_most_filter
-from . import at_least_filter
-from . import ceil_filter
-from . import floor_filter
-from . import divided_by_filter
 from . import minus_filter
-from . import plus_filter
-from . import round_filter
-from . import times_filter
 from . import modulo_filter
-from . import size_filter
-from . import default_filter
-from . import date_filter
-from . import capitalize_filter
-from . import append_filter
-from . import downcase_filter
-from . import escape_filter
-from . import escape_once_filter
-from . import lstrip_filter
 from . import newline_to_br_filter
+from . import not_liquid
+from . import output_statement
+from . import plus_filter
 from . import prepend_filter
+from . import range_objects
+from . import raw_tag
 from . import remove_filter
 from . import remove_first_filter
 from . import remove_last_filter
+from . import render_tag
 from . import replace_filter
 from . import replace_first_filter
 from . import replace_last_filter
+from . import reverse_filter
+from . import round_filter
+from . import rstrip_filter
+from . import size_filter
 from . import slice_filter
+from . import sort_filter
+from . import sort_natural_filter
+from . import special
 from . import split_filter
-from . import upcase_filter
 from . import strip_filter
-from . import rstrip_filter
 from . import strip_html_filter
 from . import strip_newlines_filter
+from . import tablerow_tag
+from . import times_filter
 from . import truncate_filter
 from . import truncatewords_filter
-from . import url_encode_filter
+from . import uniq_filter
+from . import unless_tag
+from . import upcase_filter
 from . import url_decode_filter
-from . import base64_encode_filter
-from . import base64_decode_filter
-from . import base64_url_safe_encode_filter
-from . import base64_url_safe_decode_filter
-
-from . import range_objects
-from . import special
-from . import identifiers
+from . import url_encode_filter
+from . import where_filter
+from . import whitespace_control
 
 test_cases = [
     assign_tag,
diff --git a/liquid/golden/case.py b/liquid/golden/case.py
index c846c9b8..647fb67e 100644
--- a/liquid/golden/case.py
+++ b/liquid/golden/case.py
@@ -1,12 +1,10 @@
 """Golden test case definition."""
 from dataclasses import dataclass
 from dataclasses import field
-
 from typing import Any
 from typing import Dict
 
 
-# pylint: disable=too-many-instance-attributes
 @dataclass
 class Case:
     """Test case dataclass to help with table driven tests."""
@@ -14,7 +12,7 @@ class Case:
     description: str
     template: str
     expect: str
-    globals: Dict[str, Any] = field(default_factory=dict)
+    globals: Dict[str, Any] = field(default_factory=dict)  # noqa: A003
     partials: Dict[str, Any] = field(default_factory=dict)
     standard: bool = True
     error: bool = False
diff --git a/liquid/golden/echo_tag.py b/liquid/golden/echo_tag.py
index 458cf7be..98ea5494 100644
--- a/liquid/golden/echo_tag.py
+++ b/liquid/golden/echo_tag.py
@@ -1,6 +1,7 @@
 """Golden tests cases for testing liquid's `echo` tag."""
 
 import datetime
+
 from liquid.golden.case import Case
 
 cases = [
diff --git a/liquid/golden/include_tag.py b/liquid/golden/include_tag.py
index a5fd5b21..1004a44b 100644
--- a/liquid/golden/include_tag.py
+++ b/liquid/golden/include_tag.py
@@ -1,7 +1,7 @@
 """Golden tests cases for testing liquid's `include` tag."""
 
-from liquid.golden.case import Case
 from liquid.golden.case import TEMPLATE_DROP_ATTRS
+from liquid.golden.case import Case
 
 cases = [
     Case(
diff --git a/liquid/golden/output_statement.py b/liquid/golden/output_statement.py
index cb6e13b8..8cf359ad 100644
--- a/liquid/golden/output_statement.py
+++ b/liquid/golden/output_statement.py
@@ -1,6 +1,7 @@
 """Golden tests cases for testing liquid output statements."""
 
 import datetime
+
 from liquid.golden.case import Case
 
 cases = [
@@ -119,7 +120,7 @@
     ),
     Case(
         description=(
-            "render a default given a literal false " "with 'allow false' equal to true"
+            "render a default given a literal false with 'allow false' equal to true"
         ),
         template=r"{{ false | default: 'bar', allow_false: true }}",
         expect="false",
diff --git a/liquid/golden/render_tag.py b/liquid/golden/render_tag.py
index 42c2c1ca..49995dec 100644
--- a/liquid/golden/render_tag.py
+++ b/liquid/golden/render_tag.py
@@ -1,7 +1,7 @@
 """Golden tests cases for testing liquid's `render` tag."""
 
-from liquid.golden.case import Case
 from liquid.golden.case import TEMPLATE_DROP_ATTRS
+from liquid.golden.case import Case
 
 cases = [
     Case(
diff --git a/liquid/lex.py b/liquid/lex.py
index f8bc0de2..17f5d494 100644
--- a/liquid/lex.py
+++ b/liquid/lex.py
@@ -14,62 +14,59 @@
 """
 
 from __future__ import annotations
-import re
 
+import re
 from functools import lru_cache
 from functools import partial
-
-from typing import Iterator
-from typing import Tuple
-from typing import Iterable
+from typing import Callable
 from typing import Collection
+from typing import Iterable
+from typing import Iterator
 from typing import Pattern
-from typing import Callable
+from typing import Tuple
 
 from liquid.exceptions import LiquidSyntaxError
-
-from liquid.token import TOKEN_ILLEGAL
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_EXPRESSION
-from liquid.token import TOKEN_STATEMENT
-from liquid.token import TOKEN_LITERAL
-from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_STRING
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_FLOAT
-from liquid.token import TOKEN_EMPTY
-from liquid.token import TOKEN_NIL
-from liquid.token import TOKEN_NULL
-from liquid.token import TOKEN_BLANK
-from liquid.token import TOKEN_WITH
-from liquid.token import TOKEN_FOR
+from liquid.token import TOKEN_AND
 from liquid.token import TOKEN_AS
+from liquid.token import TOKEN_ASSIGN
+from liquid.token import TOKEN_BLANK
 from liquid.token import TOKEN_BY
-from liquid.token import TOKEN_NEGATIVE
-from liquid.token import TOKEN_TRUE
-from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_COLS
+from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_CONTAINS
+from liquid.token import TOKEN_CONTINUE
+from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_EMPTY
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_FLOAT
+from liquid.token import TOKEN_FOR
+from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_ILLEGAL
 from liquid.token import TOKEN_IN
-from liquid.token import TOKEN_LPAREN
-from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_RANGE
+from liquid.token import TOKEN_INTEGER
+from liquid.token import TOKEN_LBRACKET
 from liquid.token import TOKEN_LIMIT
+from liquid.token import TOKEN_LITERAL
+from liquid.token import TOKEN_LPAREN
+from liquid.token import TOKEN_NEGATIVE
+from liquid.token import TOKEN_NIL
+from liquid.token import TOKEN_NULL
 from liquid.token import TOKEN_OFFSET
-from liquid.token import TOKEN_REVERSED
-from liquid.token import TOKEN_CONTINUE
-from liquid.token import TOKEN_COLS
+from liquid.token import TOKEN_OR
 from liquid.token import TOKEN_PIPE
-from liquid.token import TOKEN_COLON
-from liquid.token import TOKEN_COMMA
-from liquid.token import TOKEN_DOT
-from liquid.token import TOKEN_LBRACKET
+from liquid.token import TOKEN_RANGE
 from liquid.token import TOKEN_RBRACKET
-from liquid.token import TOKEN_ASSIGN
-from liquid.token import TOKEN_AND
-from liquid.token import TOKEN_OR
-from liquid.token import operators
+from liquid.token import TOKEN_REVERSED
+from liquid.token import TOKEN_RPAREN
+from liquid.token import TOKEN_STATEMENT
+from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TAG
+from liquid.token import TOKEN_TRUE
+from liquid.token import TOKEN_WITH
 from liquid.token import Token
-
+from liquid.token import operators
 
 __all__ = (
     "tokenize_assignment_expression",
@@ -229,7 +226,6 @@
 )
 
 
-# pylint: disable=too-many-locals
 def compile_liquid_rules(
     tag_start_string: str = r"{%",
     tag_end_string: str = r"%}",
diff --git a/liquid/limits.py b/liquid/limits.py
index 6991e73d..bc037d49 100644
--- a/liquid/limits.py
+++ b/liquid/limits.py
@@ -8,12 +8,10 @@
 
 import os
 import sys
-
 from typing import Any
 
 from liquid.exceptions import LiquidValueError
 
-
 _MIN_STR_INT = 640
 _DEFAULT_MAX_STR_INT = 4300
 
@@ -34,7 +32,7 @@ def _init_int_max_str_digits() -> int:
 
     if not env.isdigit():
         raise TypeError(
-            f"{var}: invalid limit; must be >= " f"{_MIN_STR_INT} or 0 for unlimited"
+            f"{var}: invalid limit; must be >= {_MIN_STR_INT} or 0 for unlimited"
         )
 
     max_digits = int(env)
diff --git a/liquid/loaders.py b/liquid/loaders.py
index 47746501..f247c234 100644
--- a/liquid/loaders.py
+++ b/liquid/loaders.py
@@ -1,56 +1,46 @@
-"""Base class and file system implementation for loading template sources.
-
-Modelled after Jinja2 template loaders.
-See https://github.com/pallets/jinja/blob/master/src/jinja2/loaders.py
-"""
+"""Base class and file system implementation for loading template sources."""
 from __future__ import annotations
 
 import asyncio
 import os
-
 from abc import ABC
-
 from collections import abc
 from functools import partial
 from pathlib import Path
-
+from typing import TYPE_CHECKING
 from typing import Awaitable
-from typing import NamedTuple
 from typing import Callable
 from typing import Dict
 from typing import Iterable
 from typing import List
 from typing import Mapping
+from typing import NamedTuple
 from typing import Optional
 from typing import Tuple
 from typing import Union
-from typing import TYPE_CHECKING
 
-from liquid.template import BoundTemplate
 from liquid.exceptions import TemplateNotFound
 
-
-if TYPE_CHECKING:  # pragma: no cover
-    from liquid import Environment
+if TYPE_CHECKING:
     from liquid import Context
+    from liquid import Environment
+    from liquid.template import BoundTemplate
+
+# ruff: noqa: D102 D101
 
 UpToDate = Union[Callable[[], bool], Callable[[], Awaitable[bool]], None]
 
 
 class TemplateSource(NamedTuple):
-    """A Liquid template source as returned by the ``get_source`` method of a `loader`.
-
-    :param source: The liquid template source code.
-    :type source: str
-    :param filename: The liquid template file name or other string identifying its
-        origin.
-    :type filename: str
-    :param uptodate: Optional callable that will return ``True`` if the template is up
-        to date, or ``False`` if it needs to be reloaded. Defaults to ``None``.
-    :type uptodate: Union[Callable[[], bool], Callable[[], Awaitable[bool]], None]
-    :param matter: Optional mapping containing variables associated with the template.
-        Could be "front matter" or other meta data.
-    :type matter: Optional[Mapping[str, object]]
+    """A Liquid template source as returned by the `get_source` method of a `loader`.
+
+    Attributes:
+        source: The liquid template source code.
+        filename: The liquid template file name or other string identifying its origin.
+        uptodate: Optional callable that will return `True` if the template is up to
+            date, or `False` if it needs to be reloaded.
+        matter: Optional mapping containing variables associated with the template.
+            Could be "front matter" or other meta data.
     """
 
     source: str
@@ -59,8 +49,7 @@ class TemplateSource(NamedTuple):
     matter: Optional[Mapping[str, object]] = None
 
 
-# flake8: noqa
-class BaseLoader(ABC):
+class BaseLoader(ABC):  # noqa: B024
     """Base template loader from which all template loaders are derived."""
 
     def get_source(
@@ -70,13 +59,9 @@ def get_source(
     ) -> TemplateSource:
         """Get the template source, filename and reload helper for a template.
 
-        :param env: The :class:`Environment` attempting to load the template source
-            text.
-        :type env: liquid.Environment
-        :param template_name: A name or identifier for a template's source text.
-        :type template_name: str
-        :returns: Template source text and meta data.
-        :rtype: TemplateSource
+        Args:
+            env: The `Environment` attempting to load the template source text.
+            template_name: A name or identifier for a template's source text.
         """
         raise NotImplementedError("template loaders must implement a get_source method")
 
@@ -85,70 +70,67 @@ async def get_source_async(
         env: Environment,
         template_name: str,
     ) -> TemplateSource:
-        """An async version of `get_source`. The default implementation delegates to
-        `get_source()`."""
+        """An async version of `get_source`.
+
+        The default implementation delegates to `get_source()`.
+        """
         return self.get_source(env, template_name)
 
-    # pylint: disable=unused-argument
     def get_source_with_args(
         self,
         env: Environment,
         template_name: str,
-        **kwargs: object,
+        **kwargs: object,  # noqa: ARG002
     ) -> TemplateSource:
         """Get template source text, optionally referencing arbitrary keyword arguments.
 
         Keyword arguments can be useful for multi-user environments where you need to
         modify a template loader's search space for a given user.
 
-        By default, this method delegates to :meth:`get_source`, ignoring any keyword
+        By default, this method delegates to `get_source`, ignoring any keyword
         arguments.
         """
         return self.get_source(env, template_name)
 
-    # pylint: disable=unused-argument
     async def get_source_with_args_async(
         self, env: Environment, template_name: str, **kwargs: object
     ) -> TemplateSource:
-        """An async version of :meth:`get_source_with_args`."""
+        """An async version of `get_source_with_args`."""
         return self.get_source_with_args(env, template_name, **kwargs)
 
-    # pylint: disable=unused-argument
     def get_source_with_context(
         self,
         context: Context,
         template_name: str,
-        **kwargs: str,
+        **kwargs: str,  # noqa: ARG002
     ) -> TemplateSource:
         """Get a template's source, optionally referencing a render context."""
         return self.get_source(context.env, template_name)
 
-    # pylint: disable=unused-argument
     async def get_source_with_context_async(
         self,
         context: Context,
         template_name: str,
-        **kwargs: str,
+        **kwargs: str,  # noqa: ARG002
     ) -> TemplateSource:
         """An async version of `get_source_with_context`."""
         return await self.get_source_async(context.env, template_name)
 
-    # pylint: disable=redefined-builtin
     def load(
         self,
         env: Environment,
         name: str,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
     ) -> BoundTemplate:
-        """Load and parse a template. Used internally by `Environment` to load a
-        template source. Delegates to `get_source`.
+        """Load and parse a template.
 
-        A custom loaders would typically implement `get_source` rather than overriding
-        `load`.
+        Used internally by `Environment` to load a template source. Delegates to
+        `get_source`. A custom loaders would typically implement `get_source` rather
+        than overriding `load`.
         """
         try:
             source, filename, uptodate, matter = self.get_source(env, name)
-        except Exception as err:
+        except Exception as err:  # noqa: BLE001
             raise TemplateNotFound(name) from err
 
         template = env.from_string(
@@ -165,13 +147,13 @@ async def load_async(
         self,
         env: Environment,
         name: str,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
     ) -> BoundTemplate:
         """An async version of `load`."""
         try:
             template_source = await self.get_source_async(env, name)
             source, filename, uptodate, matter = template_source
-        except Exception as err:
+        except Exception as err:  # noqa: BLE001
             raise TemplateNotFound(name) from err
 
         template = env.from_string(
@@ -188,22 +170,21 @@ def load_with_args(
         self,
         env: Environment,
         name: str,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
         **kwargs: object,
     ) -> BoundTemplate:
-        """Load and parse template source text, optionally referencing extra keyword
-        arguments.
+        """Load template source text, optionally referencing extra keyword arguments.
 
-        Most custom loaders will want to override :meth:`get_source_with_args` rather
-        than this method. For example, you might want to override ``load_with_args``
-        and :meth:`get_source_with_args` when implementing a custom caching loader.
-        Where cache handling happens in ``load_*`` methods.
+        Most custom loaders will want to override `get_source_with_args()` rather than
+        this method. For example, you might want to override `load_with_args()` and
+        `get_source_with_args()` when implementing a custom caching loader. Where cache
+        handling happens in `load_*` methods.
         """
         try:
             source, filename, uptodate, matter = self.get_source_with_args(
                 env, name, **kwargs
             )
-        except Exception as err:
+        except Exception as err:  # noqa: BLE001
             raise TemplateNotFound(name) from err
 
         template = env.from_string(
@@ -220,14 +201,14 @@ async def load_with_args_async(
         self,
         env: Environment,
         name: str,
-        globals: Optional[Mapping[str, object]] = None,
+        globals: Optional[Mapping[str, object]] = None,  # noqa: A002
         **kwargs: object,
     ) -> BoundTemplate:
-        """An async version of :meth:`load_with_args`."""
+        """An async version of `load_with_args()`."""
         try:
             template_source = await self.get_source_with_args_async(env, name, **kwargs)
             source, filename, uptodate, matter = template_source
-        except Exception as err:
+        except Exception as err:  # noqa: BLE001
             raise TemplateNotFound(name) from err
 
         template = env.from_string(
@@ -251,7 +232,7 @@ def load_with_context(
             source, filename, uptodate, matter = self.get_source_with_context(
                 context, name, **kwargs
             )
-        except Exception as err:
+        except Exception as err:  # noqa: BLE001
             raise TemplateNotFound(name) from err
 
         template = context.env.from_string(
@@ -278,7 +259,7 @@ async def load_with_context_async(
                 uptodate,
                 matter,
             ) = await self.get_source_with_context_async(context, name, **kwargs)
-        except Exception as err:
+        except Exception as err:  # noqa: BLE001
             raise TemplateNotFound(name) from err
 
         template = context.env.from_string(
@@ -295,11 +276,9 @@ async def load_with_context_async(
 class FileSystemLoader(BaseLoader):
     """A loader that loads templates from one or more directories on the file system.
 
-    :param search_path: One or more paths to search.
-    :type search_path: Union[str, Path, Iterable[Union[str, Path]]]
-    :param encoding: Open template files with the given encoding. Defaults to
-        ``"utf-8"``.
-    :type encoding: str
+    Args:
+        search_path: One or more paths to search.
+        encoding: Open template files with the given encoding.
     """
 
     def __init__(
@@ -352,13 +331,12 @@ def _uptodate(source_path: Path, mtime: float) -> bool:
 
     @staticmethod
     async def _uptodate_async(source_path: Path, mtime: float) -> bool:
-        uptodate = await asyncio.get_running_loop().run_in_executor(
+        return await asyncio.get_running_loop().run_in_executor(
             None, lambda: mtime == source_path.stat().st_mtime
         )
-        return uptodate
 
     async def get_source_async(
-        self, env: Environment, template_name: str
+        self, _: Environment, template_name: str
     ) -> TemplateSource:
         loop = asyncio.get_running_loop()
         source_path = await loop.run_in_executor(None, self.resolve_path, template_name)
@@ -371,14 +349,10 @@ async def get_source_async(
 class FileExtensionLoader(FileSystemLoader):
     """A file system loader that adds a file name extension if one is missing.
 
-    :param search_path: One or more paths to search.
-    :type search_path: Union[str, Path, Iterable[Union[str, Path]]]
-    :param encoding: Open template files with the given encoding. Defaults to
-        ``"utf-8"``.
-    :type encoding: str
-    :param ext: A default file extension. Should include a leading period. Defaults to
-        ``.liquid``.
-    :type ext: str
+    : Args:
+        search_path: One or more paths to search.
+        encoding: Open template files with the given encoding.
+        ext: A default file extension. Should include a leading period.
     """
 
     def __init__(
@@ -410,13 +384,10 @@ def resolve_path(self, template_name: str) -> Path:
 
 
 class DictLoader(BaseLoader):
-    """A loader that loads templates from a dictionary mapping template names to
-    template source strings. If the given dictionary is empty,
-    :meth:`liquid.Environment.get_template` will always raise a
-    :class:`liquid.exceptions.TemplateNotFound` exception.
+    """A loader that loads templates from a dictionary.
 
-    :param templates: A dictionary mapping template names to template source strings.
-    :type templates: Dict[str, str]
+    Args:
+        templates: A dictionary mapping template names to template source strings.
     """
 
     def __init__(self, templates: Dict[str, str]):
@@ -433,12 +404,10 @@ def get_source(self, _: Environment, template_name: str) -> TemplateSource:
 
 
 class ChoiceLoader(BaseLoader):
-    """A template loader that will try each of a list of loaders until a template is
-    found, or raise a :class:`liquid.exceptions.TemplateNotFound` exception if none of
-    the loaders could find the template.
+    """A template loader that will try each of a list of loaders in turn.
 
-    :param loaders: A list of loaders implementing :class:`liquid.loaders.BaseLoader`.
-    :type loaders: List[BaseLoader]
+    Args:
+        loaders: A list of loaders implementing `liquid.loaders.BaseLoader`.
     """
 
     def __init__(self, loaders: List[BaseLoader]):
diff --git a/liquid/mode.py b/liquid/mode.py
index ab0962ce..a2a40e63 100644
--- a/liquid/mode.py
+++ b/liquid/mode.py
@@ -1,6 +1,7 @@
 """Template error tolerance modes."""
 
-from enum import IntEnum, auto
+from enum import IntEnum
+from enum import auto
 
 
 class Mode(IntEnum):
diff --git a/liquid/output.py b/liquid/output.py
index 4368c76b..57d4e055 100644
--- a/liquid/output.py
+++ b/liquid/output.py
@@ -18,7 +18,7 @@ def __init__(
         self.limit = limit
         self.size = 0
 
-    def write(self, __s: str) -> int:
+    def write(self, __s: str) -> int:  # noqa: D102
         if __s:
             self.size += len(__s.encode("utf-8"))
             if self.size > self.limit:
diff --git a/liquid/parse.py b/liquid/parse.py
index 6f3654cd..06c3f0a5 100644
--- a/liquid/parse.py
+++ b/liquid/parse.py
@@ -20,78 +20,73 @@
 
 from __future__ import annotations
 
-from enum import IntEnum, auto
+from enum import IntEnum
+from enum import auto
 from functools import lru_cache
-
-from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Container
 from typing import List
-from typing import Union
-from typing import Optional
 from typing import NamedTuple
-from typing import Container
-from typing import TYPE_CHECKING
+from typing import Optional
+from typing import Tuple
+from typing import Union
 
 from liquid import ast
-
 from liquid import expression
+from liquid.exceptions import Error
+from liquid.exceptions import LiquidSyntaxError
 from liquid.expression import IdentifierPathElement
-
 from liquid.lex import tokenize_boolean_expression
-from liquid.lex import tokenize_loop_expression
 from liquid.lex import tokenize_filtered_expression
-
+from liquid.lex import tokenize_loop_expression
 from liquid.limits import to_int
 from liquid.stream import TokenStream
-
-from liquid.exceptions import LiquidSyntaxError
-from liquid.exceptions import Error
-
-from liquid.token import TOKEN_ILLEGAL
+from liquid.token import TOKEN_AND
+from liquid.token import TOKEN_ASSIGN
+from liquid.token import TOKEN_BLANK
+from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_COLS
+from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_CONTAINS
+from liquid.token import TOKEN_CONTINUE
+from liquid.token import TOKEN_DOT
+from liquid.token import TOKEN_EMPTY
 from liquid.token import TOKEN_EOF
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_STATEMENT
-from liquid.token import TOKEN_LITERAL
+from liquid.token import TOKEN_EQ
+from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_FLOAT
+from liquid.token import TOKEN_GE
+from liquid.token import TOKEN_GT
 from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_ILLEGAL
+from liquid.token import TOKEN_IN
 from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_FLOAT
-from liquid.token import TOKEN_EMPTY
+from liquid.token import TOKEN_LBRACKET
+from liquid.token import TOKEN_LE
+from liquid.token import TOKEN_LG
+from liquid.token import TOKEN_LIMIT
+from liquid.token import TOKEN_LITERAL
+from liquid.token import TOKEN_LPAREN
+from liquid.token import TOKEN_LT
+from liquid.token import TOKEN_NE
+from liquid.token import TOKEN_NEGATIVE
 from liquid.token import TOKEN_NIL
 from liquid.token import TOKEN_NULL
-from liquid.token import TOKEN_BLANK
-from liquid.token import TOKEN_NEGATIVE
-from liquid.token import TOKEN_TRUE
-from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_CONTAINS
-from liquid.token import TOKEN_IN
-from liquid.token import TOKEN_LPAREN
-from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_RANGE
-from liquid.token import TOKEN_LIMIT
 from liquid.token import TOKEN_OFFSET
-from liquid.token import TOKEN_REVERSED
-from liquid.token import TOKEN_CONTINUE
-from liquid.token import TOKEN_COLS
+from liquid.token import TOKEN_OR
 from liquid.token import TOKEN_PIPE
-from liquid.token import TOKEN_COLON
-from liquid.token import TOKEN_COMMA
-from liquid.token import TOKEN_DOT
-from liquid.token import TOKEN_LBRACKET
+from liquid.token import TOKEN_RANGE
 from liquid.token import TOKEN_RBRACKET
-from liquid.token import TOKEN_ASSIGN
-from liquid.token import TOKEN_AND
-from liquid.token import TOKEN_OR
-from liquid.token import TOKEN_EQ
-from liquid.token import TOKEN_NE
-from liquid.token import TOKEN_LG
-from liquid.token import TOKEN_LT
-from liquid.token import TOKEN_GT
-from liquid.token import TOKEN_LE
-from liquid.token import TOKEN_GE
-from liquid.token import reverse_operators
+from liquid.token import TOKEN_REVERSED
+from liquid.token import TOKEN_RPAREN
+from liquid.token import TOKEN_STATEMENT
+from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TAG
+from liquid.token import TOKEN_TRUE
 from liquid.token import Token
+from liquid.token import reverse_operators
 
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
 
 
@@ -143,8 +138,11 @@ def parse_statement(self, stream: TokenStream) -> ast.Node:
         return node
 
     def parse_block(self, stream: TokenStream, end: Container[str]) -> ast.BlockNode:
-        """Parse multiple nodes from a stream of tokens. Stop parsing nodes when we
-        find a token in `end` or we reach the end of the stream."""
+        """Parse multiple nodes from a stream of tokens.
+
+        Stop parsing nodes when we find a token in `end` or we reach the end of the
+        stream.
+        """
         block = ast.BlockNode(stream.current)
         statements = block.statements
 
@@ -200,14 +198,18 @@ def _expect(tok: Token, typ: str, value: Optional[str] = None) -> None:
 
 
 def expect(stream: TokenStream, typ: str, value: Optional[str] = None) -> None:
-    """Raise an exception if the current token in the stream does not match the given
-    type and value."""
+    """Check the current token in the stream matches the given type and value.
+
+    Raises a `LiquidSyntaxError` if they don't.
+    """
     _expect(stream.current, typ, value)
 
 
 def expect_peek(stream: TokenStream, typ: str, value: Optional[str] = None) -> None:
-    """Raise an exception if the next token in the stream does not match the given
-    type and value."""
+    """Check the next token in the stream matches the given type and value.
+
+    Raises a `LiquidSyntaxError` if they don't.
+    """
     _expect(stream.peek, typ, value)
 
 
@@ -380,12 +382,13 @@ def parse_identifier(stream: TokenStream) -> expression.Identifier:
     return expression.Identifier(path)
 
 
-def parse_string_or_identifier(
+def parse_string_or_identifier(  # pragma: no cover
     stream: TokenStream,
     linenum: Optional[int] = None,
 ) -> expression.Expression:
-    """Parse an expression from a stream of tokens. If the stream is not at a string or
-    identifier expression, raise a syntax error.
+    """Parse an expression from a stream of tokens.
+
+    If the stream is not at a string or identifier expression, raise a syntax error.
     """
     if stream.current.type == TOKEN_IDENTIFIER:
         expr: expression.Expression = parse_identifier(stream)
@@ -403,8 +406,11 @@ def parse_unchained_identifier(
     stream: TokenStream,
     linenum: Optional[int] = None,
 ) -> expression.Identifier:
-    """Parse an identifier from a stream of tokens. If the stream is not at an
-    identifier or the identifier is chained, raise a syntax error."""
+    """Parse an identifier from a stream of tokens.
+
+    If the stream is not at an identifier or the identifier is chained, raise a
+    syntax error
+    """
     expect(stream, TOKEN_IDENTIFIER)
     tok = stream.current
     ident = parse_identifier(stream)
@@ -437,8 +443,11 @@ class RangeOption(NamedTuple):
 
 
 def parse_range_argument(stream: TokenStream) -> RangeArg:
-    """Parse a loop argument value from a stream of tokens. If the stream is not at a
-    valid token for a loop argument value, raise a syntax error."""
+    """Parse a loop argument value from a stream of tokens.
+
+    If the stream is not at a valid token for a loop argument value, raise a syntax
+    error.
+    """
     if stream.current.type == TOKEN_IDENTIFIER:
         arg: RangeArg = parse_identifier(stream)
     elif stream.current.type == TOKEN_INTEGER:
@@ -456,8 +465,10 @@ def parse_range_argument(stream: TokenStream) -> RangeArg:
 
 
 def parse_range_option(stream: TokenStream) -> RangeOption:
-    """Parse a loop keyword argument from a stream of tokens. If the stream is not at a
-    valid loop keyword argument, raise a syntax error."""
+    """Parse a loop keyword argument from a stream of tokens.
+
+    If the stream is not at a valid loop keyword argument, raise a syntax error.
+    """
     tok = stream.current
     if stream.current.type in (TOKEN_LIMIT, TOKEN_OFFSET, TOKEN_COLS):
         expect_peek(stream, TOKEN_COLON)
@@ -635,13 +646,11 @@ def parse_prefix_expression(
         tok = stream.current
         stream.next_token()
 
-        exp = expression.PrefixExpression(
+        return expression.PrefixExpression(
             tok.value,
             right=self.parse_expression(stream, precedence=Precedence.PREFIX),
         )
 
-        return exp
-
     def parse_infix_expression(
         self,
         stream: TokenStream,
@@ -652,12 +661,11 @@ def parse_infix_expression(
         precedence = self.current_precedence(stream)
         stream.next_token()
 
-        exp = expression.InfixExpression(
+        return expression.InfixExpression(
             left=left,
             operator=tok.value,
             right=self.parse_expression(stream, precedence),
         )
-        return exp
 
     def parse_expression(
         self,
@@ -709,7 +717,9 @@ def parse_filtered_expression(
         filters = self.parse_filters(stream)
         return expression.FilteredExpression(expr, filters)
 
-    def parse_boolean_expression_value(self, value: str) -> expression.Expression:
+    def parse_boolean_expression_value(
+        self, value: str
+    ) -> expression.Expression:  # pragma: no cover
         """Parse the given string as a boolean expression."""
         return self.parse_boolean_expression(
             TokenStream(tokenize_boolean_expression(value))
diff --git a/liquid/static_analysis.py b/liquid/static_analysis.py
index 4584e64b..70e6da12 100644
--- a/liquid/static_analysis.py
+++ b/liquid/static_analysis.py
@@ -1,57 +1,49 @@
 """Analyze template variables, tags and filters by traversing a template's AST."""
 from __future__ import annotations
-import re
 
+import re
 from collections import defaultdict
-
+from typing import TYPE_CHECKING
 from typing import DefaultDict
 from typing import Dict
 from typing import List
 from typing import Optional
 from typing import Set
 from typing import Tuple
-from typing import TYPE_CHECKING
-
-from typing_extensions import Literal
 
 from liquid.ast import BlockNode
 from liquid.ast import ChildNode
 from liquid.ast import Node
 from liquid.ast import ParseTree
-
 from liquid.context import Context
 from liquid.context import ReadOnlyChainMap
-
 from liquid.exceptions import StopRender
 from liquid.exceptions import TemplateInheritanceError
 from liquid.exceptions import TemplateNotFound
 from liquid.exceptions import TemplateTraversalError
-
 from liquid.expression import Expression
 from liquid.expression import FilteredExpression
 from liquid.expression import Identifier
 from liquid.expression import IdentifierPathElement
 from liquid.expression import IdentifierTuple
 from liquid.expression import StringLiteral
-
 from liquid.extra.tags.extends import BlockNode as InheritanceBlockNode
 from liquid.extra.tags.extends import _BlockStackItem
 from liquid.extra.tags.extends import stack_blocks
-
 from liquid.token import TOKEN_TAG
 
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
+    from typing_extensions import Literal
+
     from liquid import BoundTemplate
 
 RE_SPLIT_IDENT = re.compile(r"(\.|\[)")
 
 
 class ReferencedVariable(str):
-    """A str subclass for variables found during static analysis that retains
-    information about a variable's parts.
-    """
+    """A str subclass for variables found during static analysis."""
 
-    def __new__(cls, obj: object) -> ReferencedVariable:
+    def __new__(cls, obj: object) -> ReferencedVariable:  # noqa: D102
         _str = super().__new__(cls, obj)
         _str.obj = obj
         return _str
@@ -66,9 +58,10 @@ def __init__(self, _: object) -> None:
 
     @property
     def parts(self) -> IdentifierTuple:
-        """A tuple representation of the variable's parts, which might contain
-        nested tuples for nested variables. For example, the variable
-        ``some[foo.bar[a.b]].other`` as a tuple would look like this:
+        """A tuple representation of the variable's parts.
+
+        `parts` might contain nested tuples for nested variables. For example, the
+        variable `some[foo.bar[a.b]].other` as a tuple would look like this:
 
             ("some", ("foo", "bar", ("a", "b")), "other.thing")
         """
@@ -89,23 +82,22 @@ def parts(self) -> IdentifierTuple:
 NameRefs = Dict[str, List[Location]]
 
 
-# pylint: disable=too-few-public-methods
 class ContextualTemplateAnalysis:
-    """The result of analyzing a template's filters and variables using
-    :meth:`BoundTemplate.analyze_with_context`.
+    """The result of analyzing a template using `BoundTemplate.analyze_with_context`.
 
     Each of the following properties is a dictionary mapping variable or filter names
     to the number of times that variable was referenced.
 
-    :ivar all_variables: All variables references along a path through the template's
-        syntax tree.
-    :ivar local_variables: The names of variables assigned using the built-in ``assign``
-        ``capture``, ``increment`` or ``decrement`` tags, or any custom tag that uses
-        ``Context.assign()``.
-    :ivar undefined_variables: The names of variables that could not be resolved. If a
-        name is referenced before it is assigned, it will appear in ``undefined`` and
-        ``assigns``.
-    :ivar filters: Names of filters found during contextual analysis.
+    Attributes:
+        all_variables: All variables references along a path through the template's
+            syntax tree.
+        local_variables: The names of variables assigned using the built-in `assign`
+            `capture`, `increment` or `decrement` tags, or any custom tag that uses
+            `Context.assign()`.
+        undefined_variables: The names of variables that could not be resolved. If a
+            name is referenced before it is assigned, it will appear in `undefined` and
+            `assigns`.
+        filters: Names of filters found during contextual analysis.
     """
 
     __slots__ = ("all_variables", "local_variables", "undefined_variables", "filters")
@@ -124,30 +116,29 @@ def __init__(
         self.filters = filters
 
 
-# pylint: disable=too-few-public-methods
 class TemplateAnalysis:
-    """The result of analyzing a template's tags, filters and variables using
-    :meth:`BoundTemplate.analyze`.
+    """The result of analyzing a template using `BoundTemplate.analyze`.
 
     Each of the following properties is a dictionary mapping variable, tag or filter
     names to a list of tuples. Each tuple holds the location of a reference to the name
-    as (<template name>, <line number>). If a name is referenced multiple times, it will
+    as (template name, line number). If a name is referenced multiple times, it will
     appear multiple times in the list. If a name is referenced before it is "assigned",
-    it will appear in ``local_variables`` and ``global_variables``.
-
-    :ivar variables: All referenced variables, whether they are in scope or not.
-        Including references to names such as ``forloop`` from the ``for`` tag.
-    :ivar local_variables: Template variables that are added to the template local
-        scope, whether they are subsequently used or not.
-    :ivar global_variables: Template variables that, on the given line number and
-        "file", are out of scope or are assumed to be "global". That is, expected to be
-        included by the application developer rather than a template author.
-    :ivar failed_visits: Names of AST ``Node`` and ``Expression`` objects that could not
-        be visited, probably because they do not implement a ``children`` method.
-    :ivar unloadable_partials: Names or identifiers of partial templates that could not
-        be loaded. This will be empty if ``follow_partials`` is ``False``.
-    :ivar filters: All filters found during static analysis.
-    :ivar tags: All tags found during static analysis.
+    it will appear in `local_variables` and `global_variables`.
+
+    Attributes:
+        variables: All referenced variables, whether they are in scope or not. Including
+            references to names such as `forloop` from the `for` tag.
+        local_variables: Template variables that are added to the template local scope,
+            whether they are subsequently used or not.
+        global_variables: Template variables that, on the given line number and "file",
+            are out of scope or are assumed to be "global". That is, expected to be
+            included by the application developer rather than a template author.
+        failed_visits: Names of AST `Node` and `Expression` objects that could not be
+            visited, probably because they do not implement a `children` method.
+        unloadable_partials: Names or identifiers of partial templates that could not
+            be loaded. This will be empty if `follow_partials` is `False`.
+        filters: All filters found during static analysis.
+        tags: All tags found during static analysis.
     """
 
     __slots__ = (
@@ -180,24 +171,22 @@ def __init__(
         self.tags = tags
 
 
-# pylint: disable=too-many-instance-attributes
 class _TemplateCounter:
     """Count references to variable names in a Liquid template.
 
-    :param template: The Liquid template to analyze.
-    :type template: liquid.BoundTemplate
-    :param follow_partials: If ``True``, the reference counter will try to load partial
-        templates and count variable references in those partials too. Default's to
-        ``True``.
-    :type follow_partials: bool
-    :param raise_for_failures: If ``True``, will raise an exception if an ``ast.Node``
-        or ``expression.Expression`` does not define a ``children()`` method, or if a
-        partial template can not be loaded.
-
-        When ``False``, no exception is raised and a mapping of failed nodes/expressions
-        is available as the ``failed_visits`` property. A mapping of unloadable partial
-        templates are stored in the ``unloadable_partials`` property.
-    :type: raised_for_failed_visits: bool
+    Args:
+        template: The Liquid template to analyze.
+        follow_partials: If `True`, the reference counter will try to load partial
+            templates and count variable references in those partials too. Default's to
+            `True`.
+        raise_for_failures: If `True`, will raise an exception if an `ast.Node` or
+            `expression.Expression` does not define a `children()` method, or if a
+            partial template can not be loaded.
+
+            When `False`, no exception is raised and a mapping of failed
+            nodes/expressions is available as the `failed_visits` property. A mapping of
+            unloadable partial templates are stored in the `unloadable_partials`
+            property.
     """
 
     def __init__(
@@ -261,7 +250,7 @@ def analyze(self) -> _TemplateCounter:
         return self
 
     async def analyze_async(self) -> _TemplateCounter:
-        """An async version of :meth:`_TemplateVariableCounter.analyze`"""
+        """An async version of `_TemplateVariableCounter.analyze()`."""
         for node in self.template.tree.statements:
             try:
                 await self._analyze_async(node)
@@ -480,7 +469,7 @@ def _analyze_render(self, child: ChildNode) -> None:
     async def _analyze_render_async(self, child: ChildNode) -> None:
         name, load_context = self._make_load_context(child, "render")
         if name is None or load_context is None:
-            return None
+            return
 
         try:
             template = await self._get_template_async(
@@ -665,10 +654,9 @@ def _stack_blocks(
         ast_extends_node, ast_block_nodes = stack_blocks(stack_context, template)
 
         # Count `extends` and `block` tags here, as we don't get the chance later.
-        if count_tags:
-            if ast_extends_node:
-                token = ast_extends_node.token()
-                self.tags[token.value].append((template_name, token.linenum))
+        if count_tags and ast_extends_node:
+            token = ast_extends_node.token()
+            self.tags[token.value].append((template_name, token.linenum))
         for ast_node in ast_block_nodes:
             token = ast_node.token()
             self.tags[token.value].append((template_name, token.linenum))
@@ -815,12 +803,11 @@ def _contains_super(self, expression: Expression) -> bool:
         if isinstance(expression, Identifier) and str(expression) == "block.super":
             return True
 
-        if isinstance(expression, FilteredExpression):
-            if (
-                isinstance(expression.expression, Identifier)
-                and str(expression.expression) == "block.super"
-            ):
-                return True
+        if isinstance(expression, FilteredExpression) and (
+            isinstance(expression.expression, Identifier)
+            and str(expression.expression) == "block.super"
+        ):
+            return True
 
         for expr in expression.children():
             return self._contains_super(expr)
@@ -878,21 +865,21 @@ def _make_template(self, item: _BlockStackItem) -> BoundTemplate:
 
 
 class References:
-    "Collects references for Template.analyze and friends"
+    """Collect references for Template.analyze and friends."""
 
     def __init__(self) -> None:
         self.variable_references: List[Identifier] = []
         self.filter_references: List[str] = []
 
     def append_variable(self, var: Identifier) -> None:
-        "Add a variable reference"
+        """Add a variable reference."""
         self.variable_references.append(var)
 
     def append_filters(self, filters: List[str]) -> None:
-        "Add references to filters"
+        """Add references to filters."""
         self.filter_references.extend(filters)
 
     def extend(self, refs: References) -> None:
-        "Incorporate references from another References"
+        """Incorporate references from another References."""
         self.variable_references.extend(refs.variable_references)
         self.filter_references.extend(refs.filter_references)
diff --git a/liquid/stream.py b/liquid/stream.py
index cab1a6cd..729a726d 100644
--- a/liquid/stream.py
+++ b/liquid/stream.py
@@ -3,14 +3,13 @@
 from __future__ import annotations
 
 from collections import deque
-
 from typing import Deque
 from typing import Iterator
 from typing import List
 
-from liquid.token import Token
-from liquid.token import TOKEN_INITIAL
 from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_INITIAL
+from liquid.token import Token
 
 
 class TokenStream:
@@ -41,7 +40,7 @@ def __next__(self) -> Token:
             tok = self.stream.current
             if tok.type is TOKEN_EOF:
                 self.stream.close()
-                raise StopIteration()
+                raise StopIteration
             next(self.stream)
             return tok
 
diff --git a/liquid/stringify.py b/liquid/stringify.py
index d92321fd..e29fd518 100644
--- a/liquid/stringify.py
+++ b/liquid/stringify.py
@@ -1,8 +1,8 @@
 """Stringify a Python object ready for output in a Liquid template."""
 from typing import Any
 
-from liquid import escape
 from liquid import Markup
+from liquid import escape
 from liquid import soft_str
 
 
diff --git a/liquid/tag.py b/liquid/tag.py
index e5d89e46..49fe7ddb 100644
--- a/liquid/tag.py
+++ b/liquid/tag.py
@@ -1,18 +1,18 @@
 """Liquid tag base class."""
 from __future__ import annotations
 
-from abc import ABC, abstractmethod
-
+from abc import ABC
+from abc import abstractmethod
 from typing import TYPE_CHECKING
 
-from liquid.ast import Node
 from liquid.ast import IllegalNode
+from liquid.ast import Node
 from liquid.exceptions import Error
 from liquid.parse import eat_block
-from liquid.stream import TokenStream
 
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
+    from liquid.stream import TokenStream
 
 
 class Tag(ABC):
diff --git a/liquid/template.py b/liquid/template.py
index cc0b8a6e..39d4fc37 100644
--- a/liquid/template.py
+++ b/liquid/template.py
@@ -1,12 +1,11 @@
 """Liquid template definition."""
 from __future__ import annotations
 
-from collections import abc
 from collections import Counter
-
+from collections import abc
 from io import StringIO
 from pathlib import Path
-
+from typing import TYPE_CHECKING
 from typing import Any
 from typing import Awaitable
 from typing import Dict
@@ -14,7 +13,6 @@
 from typing import Mapping
 from typing import Optional
 from typing import TextIO
-from typing import TYPE_CHECKING
 from typing import Union
 
 from liquid.context import Context
@@ -22,23 +20,19 @@
 from liquid.context import FutureVariableCaptureContext
 from liquid.context import ReadOnlyChainMap
 from liquid.context import VariableCaptureContext
-
 from liquid.exceptions import Error
 from liquid.exceptions import LiquidInterrupt
 from liquid.exceptions import LiquidSyntaxError
 from liquid.exceptions import StopRender
-
 from liquid.output import LimitedStringIO
-
-from liquid.static_analysis import _TemplateCounter
 from liquid.static_analysis import ContextualTemplateAnalysis
 from liquid.static_analysis import NameRefs
-from liquid.static_analysis import Refs
 from liquid.static_analysis import ReferencedVariable
+from liquid.static_analysis import Refs
 from liquid.static_analysis import TemplateAnalysis
+from liquid.static_analysis import _TemplateCounter
 
-
-if TYPE_CHECKING:  # pragma: no cover
+if TYPE_CHECKING:
     from liquid import Environment
     from liquid.ast import ParseTree
     from liquid.loaders import UpToDate
@@ -59,30 +53,22 @@
 
 
 class BoundTemplate:
-    """A liquid template that has been parsed and is bound to a
-    :class:`liquid.Environment`.
-
-    You probably don't want to instantiate :class:`BoundTemplate` directly. Use
-    :meth:`liquid.Environment.from_string` or :meth:`liquid.Environment.get_template`
-    instead.
-
-    :param env: The environment this template is bound to.
-    :type env: liquid.Environment
-    :param parse_tree: The parse tree representing this template.
-    :type parse_tree: liquid.ast.ParseTree
-    :param name: Optional name of the template. Defaults to an empty string.
-    :type name: Optional[str]
-    :param path: Optional origin path or identifier for the template.
-    :type path: Optional[Union[str, Path]]
-    :param globals: An optional mapping of context variables made available every
-        time the resulting template is rendered. Defaults to ``None``.
-    :type globals: Optional[Dict[str, object]]
-    :param matter: Optional mapping containing variables associated with the template.
-        Could be "front matter" or other meta data.
-    :type matter: Optional[Mapping[str, object]]
-    :param uptodate: Optional callable that will return ``True`` if the template is up
-        to date, or ``False`` if it needs to be reloaded. Defaults to ``None``.
-    :type uptodate: Optional[Callable[[], bool]]
+    """A liquid template that has been parsed and is bound to a `liquid.Environment`.
+
+    You probably don't want to instantiate `BoundTemplate` directly. Use
+    `liquid.Environment.from_string` or `liquid.Environment.get_template` instead.
+
+    Args:
+        env: The environment this template is bound to.
+        parse_tree: The parse tree representing this template.
+        name: Optional name of the template. Defaults to an empty string.
+        path: Optional origin path or identifier for the template.
+        globals: An optional mapping of context variables made available every
+            time the resulting template is rendered. Defaults to `None`.
+        matter: Optional mapping containing variables associated with the template.
+            Could be "front matter" or other meta data.
+        uptodate: Optional callable that will return `True` if the template is up
+            to date, or `False` if it needs to be reloaded. Defaults to `None`.
     """
 
     # Subclass `BoundTemplate` and override `context_class` to use a subclass of
@@ -90,14 +76,13 @@ class BoundTemplate:
     context_class = Context
     capture_context_class = VariableCaptureContext
 
-    # pylint: disable=redefined-builtin, too-many-arguments
     def __init__(
         self,
         env: Environment,
         parse_tree: ParseTree,
         name: str = "",
         path: Optional[Union[str, Path]] = None,
-        globals: Optional[Dict[str, object]] = None,
+        globals: Optional[Dict[str, object]] = None,  # noqa: A002
         matter: Optional[Mapping[str, object]] = None,
         uptodate: UpToDate = None,
     ):
@@ -112,7 +97,7 @@ def __init__(
     def render(self, *args: Any, **kwargs: Any) -> str:
         """Render the template with `args` and `kwargs` included in the render context.
 
-        Accepts the same arguments as the :class:`dict` constructor.
+        Accepts the same arguments as the `dict` constructor.
         """
         context = self.context_class(
             self.env,
@@ -124,7 +109,7 @@ def render(self, *args: Any, **kwargs: Any) -> str:
         return buf.getvalue()
 
     async def render_async(self, *args: Any, **kwargs: Any) -> str:
-        """An async version of :meth:`liquid.template.BoundTemplate.render`."""
+        """An async version of `liquid.template.BoundTemplate.render`."""
         context = self.context_class(
             self.env,
             globals=self.make_globals(dict(*args, **kwargs)),
@@ -150,16 +135,18 @@ def render_with_context(
     ) -> None:
         """Render the template using an existing context and output buffer.
 
-        ``args`` and ``kwargs`` are passed to the :class:`dict` constructor. The
-        resulting dictionary is added to the render context.
-
-        :param context: A render context.
-        :param buffer: File-like object to which rendered text is written.
-        :param partial: If `True`, indicates that the current template has been
-            included using either a "render" or "include" tag. Defaults to ``False``.
-        :param block_scope: If `True`, indicates that assigns, breaks and continues
-            from this template will not leak into the parent context. Defaults to
-            ``False``.
+        Args:
+            context: A render context.
+            buffer: File-like object to which rendered text is written.
+            partial: If `True`, indicates that the current template has been
+                included using either a "render" or "include" tag. Defaults to `False`.
+            block_scope: If `True`, indicates that assigns, breaks and continues
+                from this template will not leak into the parent context. Defaults to
+                `False`.
+            args: Passed to the `dict` constructor. The resulting dictionary is added to
+                the render context.
+            kwargs: Passed to the `dict` constructor. The resulting dictionary is added
+                to the render context.
         """
         # "template" could get overridden from args/kwargs, "partial" will not.
         namespace = self.make_partial_namespace(partial, dict(*args, **kwargs))
@@ -227,8 +214,7 @@ async def render_with_context_async(
 
     @property
     def is_up_to_date(self) -> bool:
-        """False if the template was modified since it was last parsed,
-        True otherwise."""
+        """`False` if the template has bee modified, `True` otherwise."""
         if not self.uptodate:
             return True
 
@@ -240,10 +226,10 @@ def is_up_to_date(self) -> bool:
         return uptodate
 
     async def is_up_to_date_async(self) -> bool:
-        """An async version of the ``is_up_to_date`` property.
+        """An async version of the `is_up_to_date` property.
 
-        If ``template.uptodate`` is a coroutine, it wil be awaited. Otherwise it will be
-        called just like ``is_up_to_date``
+        If `template.uptodate` is a coroutine, it wil be awaited. Otherwise it will be
+        called just like `is_up_to_date`
         """
         if not self.uptodate:
             return True
@@ -254,8 +240,7 @@ async def is_up_to_date_async(self) -> bool:
         return uptodate
 
     def make_globals(self, render_args: Mapping[str, object]) -> Mapping[str, object]:
-        """Return a mapping aggregated from render arguments, template globals and
-        matter variables."""
+        """Return a mapping including render arguments and template globals."""
         return ReadOnlyChainMap(
             render_args,
             self.matter,
@@ -267,8 +252,10 @@ def make_partial_namespace(
         partial: bool,
         render_args: Mapping[str, object],
     ) -> Mapping[str, object]:
-        """Return a namespace dictionary. This is used by `render_with_context` to
-        extend an existing context."""
+        """Return a namespace dictionary.
+
+        This is used by `render_with_context` to extend an existing context.
+        """
         return {**render_args, "partial": partial}
 
     def __repr__(self) -> str:
@@ -282,18 +269,15 @@ def analyze(
     ) -> TemplateAnalysis:
         """Statically analyze this template and any included/rendered templates.
 
-        :param follow_partials: If ``True``, we will try to load partial templates and
-            analyze those templates too. Default's to ``True``.
-        :type follow_partials: bool
-        :param raise_for_failures: If ``True``, will raise an exception if an
-            ``ast.Node`` or ``expression.Expression`` does not define a ``children()``
-            method, or if a partial template can not be loaded. When ``False``, no
-            exception is raised and a mapping of failed nodes and expressions is
-            available as the ``failed_visits`` property. A mapping of unloadable partial
-            templates is stored in the ``unloadable_partials`` property.
-        :type raised_for_failed_visits: bool
-        :returns: A object containing analysis results.
-        :rtype: :class:`liquid.template.TemplateAnalysis`
+        Args:
+            follow_partials: If `True`, we will try to load partial templates and
+                analyze those templates too.
+            raise_for_failures: If `True`, will raise an exception if an
+                `ast.Node` or `expression.Expression` does not define a `children()`
+                method, or if a partial template can not be loaded. When `False`, no
+                exception is raised and a mapping of failed nodes and expressions is
+                available as the `failed_visits` property. A mapping of unloadable
+                partial templates is stored in the `unloadable_partials` property.
         """
         refs = _TemplateCounter(
             self,
@@ -318,7 +302,7 @@ def analyze(
     async def analyze_async(
         self, follow_partials: bool = True, raise_for_failures: bool = True
     ) -> TemplateAnalysis:
-        """An async version of :meth:`analyze`."""
+        """An async version of `analyze`."""
         refs = await _TemplateCounter(
             self,
             follow_partials=follow_partials,
@@ -344,22 +328,19 @@ def analyze_with_context(
     ) -> ContextualTemplateAnalysis:
         """Analyze a path through this template's syntax tree given some context data.
 
-        Unlike :meth:`analyze`, this form of template analysis does perform a render,
+        Unlike `analyze`, this form of template analysis does perform a render,
         capturing template variables as we go.
 
         Python Liquid does not currently support template introspection from within a
-        render context or ``Expression`` object. Meaning line numbers and template names
+        render context or `Expression` object. Meaning line numbers and template names
         are not available. Only variable names are reported along with the number of
         times they were referenced.
 
         It's also, using this method, not currently possible to detect names added to a
-        block's scope. For example, ``forloop.index`` will be included in the results
-        object if referenced within a ``for`` loop block.
-
-        Accepts the same arguments as :meth:`render`.
+        block's scope. For example, `forloop.index` will be included in the results
+        object if referenced within a `for` loop block.
 
-        :returns: Contextual analysis results.
-        :rtype: :class:`liquid.template.ContextualTemplateAnalysis`
+        Accepts the same arguments as `render`.
         """
         context = self.capture_context_class(
             self.env, globals=self.make_globals(dict(*args, **kwargs))
@@ -376,7 +357,7 @@ def analyze_with_context(
     async def analyze_with_context_async(
         self, *args: Any, **kwargs: Any
     ) -> ContextualTemplateAnalysis:
-        """An async version of :meth:`analyze_with_context`."""
+        """An async version of `analyze_with_context`."""
         context = self.capture_context_class(
             self.env, globals=self.make_globals(dict(*args, **kwargs))
         )
@@ -391,14 +372,13 @@ async def analyze_with_context_async(
 
 
 class AwareBoundTemplate(BoundTemplate):
-    """A `BoundTemplate` subclass that automatically includes a `TemplateDrop` in the
-    global namespace."""
+    """A `BoundTemplate` subclass with a `TemplateDrop` in the global namespace."""
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:
         super().__init__(*args, **kwargs)
         self.drop = TemplateDrop(self.name, self.path)
 
-    def make_partial_namespace(
+    def make_partial_namespace(  # noqa: D102
         self,
         partial: bool,
         render_args: Mapping[str, object],
@@ -410,10 +390,9 @@ def make_partial_namespace(
 
 
 class FutureBoundTemplate(BoundTemplate):
-    """A ``BoundTemplate`` subclass configured to use the ``FutureContext`` render
-    context.
+    """A `BoundTemplate` configured to use the `FutureContext` render context.
 
-    See :class:`liquid.future.Environment`.
+    See `liquid.future.Environment`.
     """
 
     context_class = FutureContext
@@ -421,10 +400,11 @@ class FutureBoundTemplate(BoundTemplate):
 
 
 class FutureAwareBoundTemplate(AwareBoundTemplate):
-    """A ``BoundTemplate`` subclass configured to use the ``FutureContext`` render
-    context, and includes a `TemplateDrop` in each render context.
+    """A `BoundTemplate` configured to use the `FutureContext` render context.
+
+    Inserts a `TemplateDrop` into the global namespace automatically.
 
-    See :class:`liquid.future.Environment`.
+    See `liquid.future.Environment`.
     """
 
     context_class = FutureContext
diff --git a/liquid/token.py b/liquid/token.py
index ec5416c1..4f30c404 100644
--- a/liquid/token.py
+++ b/liquid/token.py
@@ -1,4 +1,4 @@
-"""Token definitions"""
+"""Token definitions."""
 
 import sys
 from typing import NamedTuple
@@ -173,7 +173,7 @@ class Token(NamedTuple):
     """A token, as generated by the template or expression lexers."""
 
     linenum: int
-    type: str
+    type: str  # noqa: A003
     value: str
 
     def test(self, typ: str) -> bool:
diff --git a/liquid/undefined.py b/liquid/undefined.py
index 8289b346..3588f596 100644
--- a/liquid/undefined.py
+++ b/liquid/undefined.py
@@ -15,8 +15,9 @@
 
 
 class Undefined(Mapping[Any, object]):
-    """The default undefined type. Always evaluates to an empty string. Can be iterated
-    over and indexed without error.
+    """The default undefined type.
+
+    Always evaluates to an empty string. Can be iterated over and indexed without error.
     """
 
     __slots__ = ("name", "obj", "hint")
@@ -74,7 +75,7 @@ def __repr__(self) -> str:  # pragma: no cover
 
 
 class StrictUndefined(Undefined):
-    """An undefined that raises an exception for everything other than ``repr``."""
+    """An undefined that raises an exception for everything other than `repr`."""
 
     __slots__ = ("msg",)
 
diff --git a/liquid/utils/__init__.py b/liquid/utils/__init__.py
index 5b77eda5..a74d086c 100644
--- a/liquid/utils/__init__.py
+++ b/liquid/utils/__init__.py
@@ -1,6 +1,11 @@
-# flake8: noqa
-# pylint: disable=useless-import-alias,missing-module-docstring
-from .cache import LRUCache as LRUCache
-from .html import strip_tags as strip_tags
-from .text import truncate_chars as truncate_chars
-from .text import truncate_words as truncate_words
+from .cache import LRUCache  # noqa: D104
+from .html import strip_tags
+from .text import truncate_chars
+from .text import truncate_words
+
+__all__ = (
+    "LRUCache",
+    "strip_tags",
+    "truncate_chars",
+    "truncate_words",
+)
diff --git a/liquid/utils/cache.py b/liquid/utils/cache.py
index 0a56cb77..c3e25366 100644
--- a/liquid/utils/cache.py
+++ b/liquid/utils/cache.py
@@ -31,11 +31,12 @@
 OF THE POSSIBILITY OF SUCH DAMAGE.
 """
 
-from collections import abc, deque
+import contextlib
+from collections import abc
+from collections import deque
 from threading import Lock
 
 
-# pylint: disable=too-many-instance-attributes,invalid-name,protected-access,consider-using-with
 class LRUCache(abc.MutableMapping):
     """A simple LRU Cache implementation."""
 
@@ -74,12 +75,12 @@ def __getnewargs__(self):  # pragma: no cover
     def copy(self):
         """Return a shallow copy of the instance."""
         rv = self.__class__(self.capacity)
-        rv._mapping.update(self._mapping)
-        rv._queue.extend(self._queue)
+        rv._mapping.update(self._mapping)  # noqa: SLF001
+        rv._queue.extend(self._queue)  # noqa: SLF001
         return rv
 
     def get(self, key: str, default: object = None):
-        """Return an item from the cache dict or `default`"""
+        """Return an item from the cache dict or `default`."""
         try:
             return self[key]
         except KeyError:
@@ -88,7 +89,7 @@ def get(self, key: str, default: object = None):
     def setdefault(self, key, default=None):
         """Set `default` if the key is not in the cache otherwise
         leave unchanged. Return the value of this key.
-        """
+        """  # noqa: D205
         try:
             return self[key]
         except KeyError:
@@ -116,29 +117,29 @@ def __repr__(self):  # pragma: no cover
         return f"<{self.__class__.__name__} {self._mapping!r}>"
 
     def __getitem__(self, key):
-        """Get an item from the cache. Moves the item up so that it has the
-        highest priority then.
+        """Get an item from the cache.
+
+        Moves the item up so that it has the highest priority then.
         Raise a `KeyError` if it does not exist.
         """
         self._wlock.acquire()
         try:
             rv = self._mapping[key]
             if self._queue[-1] != key:
-                try:
+                # if something removed the key from the container
+                # when we read, ignore the ValueError that we would
+                # get otherwise.
+                with contextlib.suppress(ValueError):
                     self._remove(key)
-                except ValueError:  # pragma: no cover
-                    # if something removed the key from the container
-                    # when we read, ignore the ValueError that we would
-                    # get otherwise.
-                    pass
                 self._append(key)
             return rv
         finally:
             self._wlock.release()
 
     def __setitem__(self, key, value):
-        """Sets the value for an item. Moves the item up so that it
-        has the highest priority then.
+        """Sets the value for an item.
+
+        Moves the item up so that it has the highest priority then.
         """
         self._wlock.acquire()
         try:
@@ -153,15 +154,14 @@ def __setitem__(self, key, value):
 
     def __delitem__(self, key):
         """Remove an item from the cache dict.
+
         Raise a `KeyError` if it does not exist.
         """
         self._wlock.acquire()
         try:
             del self._mapping[key]
-            try:
+            with contextlib.suppress(ValueError):
                 self._remove(key)
-            except ValueError:  # pragma: no cover
-                pass
         finally:
             self._wlock.release()
 
@@ -183,9 +183,7 @@ def __iter__(self):
         return reversed(tuple(self._queue))
 
     def __reversed__(self):
-        """Iterate over the keys in the cache dict, oldest items
-        coming first.
-        """
+        """Iterate over the keys in the cache dict, oldest items coming first."""
         return iter(tuple(self._queue))
 
     __copy__ = copy
diff --git a/liquid/utils/cache.pyi b/liquid/utils/cache.pyi
index f137eff8..889eaf7f 100644
--- a/liquid/utils/cache.pyi
+++ b/liquid/utils/cache.pyi
@@ -1,15 +1,13 @@
-# flake8: noqa
-
-from typing import Any
+from typing import Any  # noqa: D100
 from typing import Iterator
 from typing import MutableMapping
 
-class LRUCache(MutableMapping[Any, Any]):
+class LRUCache(MutableMapping[Any, Any]):  # noqa: D101
     def __init__(self, capacity: int) -> None: ...
     def __getitem__(self, key: Any) -> Any: ...
     def __iter__(self) -> Iterator[Any]: ...
     def __len__(self) -> int: ...
     def __setitem__(self, key: Any, value: Any) -> None: ...
     def __delitem__(self, key: Any) -> None: ...
-    def copy(self) -> LRUCache: ...
+    def copy(self) -> LRUCache: ...  # noqa: D102
     def __reversed__(self) -> Iterator[Any]: ...
diff --git a/liquid/utils/html.py b/liquid/utils/html.py
index b7b8c602..8e49cfac 100644
--- a/liquid/utils/html.py
+++ b/liquid/utils/html.py
@@ -1,13 +1,14 @@
 """Utilities for working with strings of HTML."""
 
 from html.parser import HTMLParser
-
 from typing import List
-from typing import Tuple
 from typing import Optional
+from typing import Tuple
+
+# ruff: noqa: D102
 
 
-class StripParser(HTMLParser):  # pylint: disable=abstract-method
+class StripParser(HTMLParser):
     """An HTML parser that strips out tags."""
 
     def __init__(self) -> None:
diff --git a/liquid/utils/text.py b/liquid/utils/text.py
index 419f01a3..e536426b 100644
--- a/liquid/utils/text.py
+++ b/liquid/utils/text.py
@@ -3,7 +3,8 @@
 
 def truncate_chars(val: str, num: int, end: str = "...") -> str:
     """Truncates a string if it is longer than the specified number of characters.
-    Truncated strings will end with `end`, an ellipsis by default."""
+    Truncated strings will end with `end`, an ellipsis by default.
+    """  # noqa: D205
     val_length = len(val)
     end_length = len(end)
 
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..4db466a8
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,66 @@
+site_name: Python Liquid
+site_description: A Python engine for the Liquid template language.
+
+theme:
+  name: "material"
+  palette:
+    - scheme: "default"
+      media: "(prefers-color-scheme: light)"
+      toggle:
+        icon: "material/weather-sunny"
+        name: "Switch to dark mode"
+    - scheme: "slate"
+      media: "(prefers-color-scheme: dark)"
+      primary: "blue"
+      toggle:
+        icon: "material/weather-night"
+        name: "Switch to light mode"
+  features:
+    - navigation.sections
+    - content.code.copy
+
+repo_name: jg-rp/liquid
+repo_url: https://github.com/jg-rp/liquid
+edit_uri: ""
+
+plugins:
+  - search
+  - mkdocstrings:
+      default_handler: python
+      handlers:
+        python:
+          options:
+            heading_level: 2
+            show_source: false
+            show_signature_annotations: true
+            show_root_heading: true
+            separate_signature: true
+            docstring_section_style: "spacy"
+            merge_init_into_class: true
+            show_root_full_path: false
+  - autorefs
+
+nav:
+  - Introduction: "index.md"
+  - API Reference:
+      - Rendering Templates: "api.md"
+      - Template Analysis: "analysis.md"
+      - Template Loaders: "loaders.md"
+      - Undefined Types: "undefined.md"
+      - Exceptions: "exceptions.md"
+
+markdown_extensions:
+  - admonition
+  - pymdownx.highlight:
+      anchor_linenums: true
+      line_spans: __span
+      pygments_lang_class: true
+  - pymdownx.inlinehilite
+  - pymdownx.snippets
+  - pymdownx.superfences
+
+extra_css:
+  - css/style.css
+
+watch:
+  - liquid
diff --git a/pyproject.toml b/pyproject.toml
index 864b334a..e53924b5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,214 @@
 [build-system]
-requires = ["setuptools", "wheel"]
-build-backend = "setuptools.build_meta:__legacy__"
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "python-liquid"
+dynamic = ["version"]
+description = "A Python engine for the Liquid template language."
+readme = "README.md"
+license = "MIT"
+requires-python = ">=3.7"
+authors = [{ name = "James Prior", email = "jamesgr.prior@gmail.com" }]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: MIT License",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: Implementation :: CPython",
+    "Programming Language :: Python :: Implementation :: PyPy",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+]
+dependencies = ["python-dateutil>=2.8.1", "typing-extensions>=4.2.0"]
+
+[project.optional-dependencies]
+autoescape = ["MarkupSafe>=2.0.0"]
+
+[project.urls]
+"Change Log" = "https://github.com/jg-rp/liquid/blob/main/CHANGES.md"
+Documentation = "https://jg-rp.github.io/liquid/"
+Homepage = "https://jg-rp.github.io/liquid/"
+"Issue Tracker" = "https://github.com/jg-rp/liquid/issues"
+"Source Code" = "https://github.com/jg-rp/liquid"
+
+[tool.hatch.version]
+path = "liquid/__about__.py"
+
+[tool.hatch.build.targets.sdist]
+include = ["/liquid"]
+
+[tool.hatch.envs.default]
+dependencies = [
+    "pytest",
+    "pytest-cov",
+    "black",
+    "mypy",
+    "pyyaml",
+    "types-pyyaml",
+    "twine",
+    "ruff",
+    "sphinx",
+    "mock",
+    "types-python-dateutil",
+]
+
+[tool.hatch.envs.default.scripts]
+cov = [
+    "hatch run noescape:cov",
+    "pytest --cov-append --cov-report= --cov-fail-under=97 --cov-config=pyproject.toml --cov=liquid --cov=tests {args}",
+]
+cov-html = [
+    "hatch run noescape:cov",
+    "pytest --cov-append --cov-report=html --cov-config=pyproject.toml --cov=liquid --cov=tests {args}",
+]
+no-cov = "cov --no-cov {args}"
+test = "pytest {args}"
+lint = "ruff check ."
+typing = "mypy"
+benchmark = "python -O scripts/performance.py"
+profile = "python -O scripts/performance.py --profile"
+
+[[tool.hatch.envs.test.matrix]]
+python = ["37", "38", "39", "310", "311", "pypy38", "pypy39"]
+
+[tool.hatch.envs.test.scripts]
+test = "pytest {args}"
+
+
+[tool.hatch.envs.noescape]
+template = "noescape"
+features = []
+dependencies = ["pytest", "pytest-cov", "mock"]
+
+[tool.hatch.envs.noescape.scripts]
+cov = "pytest --cov-report= --cov-config=pyproject.toml --cov=liquid --cov=tests {args}"
+test = "pytest {args}"
+
+[tool.hatch.envs.docs]
+dependencies = ["black", "mkdocs", "mkdocstrings[python]", "mkdocs-material"]
+
+[tool.hatch.envs.docs.scripts]
+build = "mkdocs build --clean --strict"
+serve = "mkdocs serve --dev-addr localhost:8000"
+
+[tool.coverage.run]
+branch = true
+parallel = true
+omit = ["tests/mocks/**/*", "liquid/__about__.py"]
+
+[tool.coverage.report]
+exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
+
+
+[tool.mypy]
+files = "liquid"
+python_version = "3.10"
+disallow_subclassing_any = true
+disallow_untyped_calls = true
+disallow_untyped_defs = true
+disallow_incomplete_defs = true
+no_implicit_optional = true
+local_partial_types = true
+no_implicit_reexport = true
+strict = true
+warn_redundant_casts = true
+warn_unused_configs = true
+warn_unused_ignores = false
+warn_return_any = true
+warn_unreachable = true
+
+[tool.ruff]
+select = [
+    "A",
+    "ARG",
+    "B",
+    "BLE",
+    "C4",
+    "D",
+    "E",
+    "F",
+    "I",
+    "ICN",
+    "ISC",
+    "N",
+    "PIE",
+    "PL",
+    "PT",
+    "Q",
+    "RET",
+    "RSE",
+    "S",
+    "SIM",
+    "SLF",
+    "T10",
+    "T20",
+    "TCH",
+    "YTT",
+]
+# TODO: review ignores
+ignore = [
+    "S105",
+    "S101",
+    "D107",
+    "D105",
+    "PLR0913",
+    "D401",
+    "N818",
+    "PT009",
+    "B905",
+]
+
+fixable = ["I", "SIM", "D202"]
+unfixable = []
+
+# Exclude a variety of commonly ignored directories.
+exclude = [
+    ".bzr",
+    ".direnv",
+    ".eggs",
+    ".git",
+    ".hg",
+    ".mypy_cache",
+    ".nox",
+    ".pants.d",
+    ".pytype",
+    ".ruff_cache",
+    ".svn",
+    ".tox",
+    ".venv",
+    "__pypackages__",
+    "_build",
+    "buck-out",
+    "build",
+    "dist",
+    "node_modules",
+    "venv",
+]
+
+# Same as Black.
+line-length = 88
+
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+# Assume Python 3.10.
+target-version = "py310"
+
+[tool.ruff.isort]
+force-single-line = true
+
+[tool.ruff.pydocstyle]
+convention = "google"
+
+
+[tool.ruff.per-file-ignores]
+"liquid/__about__.py" = ["D100"]
+"liquid/__init__.py" = ["D104", "I001"]
+"liquid/builtin/filters/__init__.py" = ["D104", "I001"]
+"liquid/builtin/tags/__init__.py" = ["D104", "I001"]
+"tests/*" = ["D100", "D101", "D104", "D103", "D102", "D209", "D205", "SIM117"]
+"scripts/__init__.py" = ["D104", "I001"]
diff --git a/liquid/expressions/include/parse.py b/scripts/__init__.py
similarity index 100%
rename from liquid/expressions/include/parse.py
rename to scripts/__init__.py
diff --git a/performance.py b/scripts/performance.py
similarity index 97%
rename from performance.py
rename to scripts/performance.py
index 446392fa..28d99f45 100644
--- a/performance.py
+++ b/scripts/performance.py
@@ -5,39 +5,34 @@
 total of 34 .liquid files, 4 of which are a theme.liquid, `Template.render`
 and/or `env.from_string` will be called 60 times per loop.
 """
-# pylint: disable=missing-function-docstring
 import cProfile
+import os
 import pathlib
-import timeit
 import sys
-
-from typing import NamedTuple
-from typing import List
-from typing import Dict
+import timeit
 from typing import Any
-from typing import Mapping
-from typing import Optional
 from typing import Callable
+from typing import Dict
 from typing import Iterator
+from typing import List
+from typing import Mapping
+from typing import NamedTuple
+from typing import Optional
 
 import yaml
 
+sys.path.insert(0, os.getcwd())
+
 from liquid.environment import Environment
-from liquid.template import BoundTemplate
-from liquid.template import AwareBoundTemplate
-from liquid.mode import Mode
+from liquid.lex import get_lexer
 from liquid.loaders import FileSystemLoader
+from liquid.mode import Mode
+from liquid.template import AwareBoundTemplate
+from liquid.template import BoundTemplate
 from liquid.token import Token
-from liquid.lex import get_lexer
-
-from tests.mocks.tags.form_tag import CommentFormTag
-from tests.mocks.tags.paginate_tag import PaginateTag
-
 from tests.mocks.filters.json import json_
-
 from tests.mocks.filters.money import money_
 from tests.mocks.filters.money import money_with_currency
-
 from tests.mocks.filters.shop import asset_url
 from tests.mocks.filters.shop import default_pagination
 from tests.mocks.filters.shop import global_asset_url
@@ -45,22 +40,24 @@
 from tests.mocks.filters.shop import link_to
 from tests.mocks.filters.shop import link_to_type
 from tests.mocks.filters.shop import link_to_vendor
+from tests.mocks.filters.shop import pluralize
+from tests.mocks.filters.shop import product_img_url
+from tests.mocks.filters.shop import script_tag
 from tests.mocks.filters.shop import shopify_asset_url
 from tests.mocks.filters.shop import stylesheet_tag
-from tests.mocks.filters.shop import script_tag
 from tests.mocks.filters.shop import url_for_type
 from tests.mocks.filters.shop import url_for_vendor
-from tests.mocks.filters.shop import product_img_url
-from tests.mocks.filters.shop import pluralize
 from tests.mocks.filters.shop import within
-
 from tests.mocks.filters.tag import highlight_active_tag
 from tests.mocks.filters.tag import link_to_add_tag
 from tests.mocks.filters.tag import link_to_remove_tag
 from tests.mocks.filters.tag import link_to_tag
-
 from tests.mocks.filters.weight import weight
 from tests.mocks.filters.weight import weight_with_unit
+from tests.mocks.tags.form_tag import CommentFormTag
+from tests.mocks.tags.paginate_tag import PaginateTag
+
+# ruff: noqa: D102 D205 D103 T201 E402
 
 
 class ThemedTemplate(NamedTuple):
@@ -139,7 +136,7 @@ def load_data() -> Dict[str, Any]:
     See https://github.com/Shopify/liquid/blob/master/performance/shopify/database.rb
     """
     with open("tests/fixtures/vision.database.yml", "r") as fd:
-        data = yaml.load(fd, Loader=yaml.Loader)
+        data = yaml.safe_load(fd)
 
     # Collections contain products, but shove a list of collections that a product
     # belongs to into each product object too.
@@ -218,11 +215,10 @@ def lex(
         list(tokenizer(bundle.template.source))
 
 
-# pylint: disable=redefined-builtin
 def parse(
     env: Environment,
     templates: List[ThemedTemplateSource],
-    globals: Optional[Mapping[str, object]] = None,
+    globals: Optional[Mapping[str, object]] = None,  # noqa: A002
 ) -> List[ThemedTemplate]:
     """Parse a list of templates using the given environment."""
     parsed_templates = []
@@ -246,11 +242,10 @@ def render(templates: List[ThemedTemplate]):
         template.render()
 
 
-# pylint: disable=redefined-builtin
 def parse_and_render(
     env: Environment,
     templates: List[ThemedTemplateSource],
-    globals: Optional[Mapping[str, object]] = None,
+    globals: Optional[Mapping[str, object]] = None,  # noqa: A002
 ):
     parsed_templates = parse(env, templates, globals=globals)
     render(parsed_templates)
@@ -306,8 +301,7 @@ def profile_parse_and_render(search_path: str):
 
 def setup_render(search_path: str) -> List[ThemedTemplate]:
     env, template_sources = setup_parse(search_path)
-    parsed_templates = parse(env, template_sources)
-    return parsed_templates
+    return parse(env, template_sources)
 
 
 def setup_parse(search_path: str):
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 03535b1a..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,69 +0,0 @@
-[metadata]
-name = python-liquid
-version = attr: liquid.__version__
-description = A Python engine for the Liquid template language.
-long_description = file: README.rst
-long_description_content_type = text/x-rst
-url = https://jg-rp.github.io/liquid/
-license = MIT
-classifiers =
-    Development Status :: 5 - Production/Stable
-    Intended Audience :: Developers
-    License :: OSI Approved :: MIT License
-    Programming Language :: Python :: 3
-    Programming Language :: Python :: 3.7
-    Programming Language :: Python :: 3.8
-    Programming Language :: Python :: 3.9
-    Programming Language :: Python :: 3.10
-    Programming Language :: Python :: 3.11
-    Programming Language :: Python :: Implementation :: CPython
-    Programming Language :: Python :: Implementation :: PyPy
-project_urls = 
-    Documentation = https://jg-rp.github.io/liquid/
-    Issue Tracker = https://github.com/jg-rp/liquid/issues
-    Source Code = https://github.com/jg-rp/liquid
-    Change Log = https://github.com/jg-rp/liquid/blob/main/CHANGES.rst
-
-[options]
-zip_safe = False
-include_package_data = True
-packages = find:
-python_requires = >=3.7
-install_requires =
-    python-dateutil>=2.8.1
-    typing-extensions>=4.2.0
-
-[options.extras_require]
-autoescape = MarkupSafe>=2.0.0
-
-[options.packages.find]
-exclude =
-    docs*
-    tests*
-
-[mypy]
-files = liquid
-python_version = 3.10
-disallow_subclassing_any = True
-disallow_untyped_calls = True
-disallow_untyped_defs = True
-disallow_incomplete_defs = True
-no_implicit_optional = True
-local_partial_types = True
-no_implicit_reexport = True
-strict = True
-warn_redundant_casts = True
-warn_unused_configs = True
-warn_unused_ignores = False
-warn_return_any = True
-warn_unreachable = True
-
-[coverage:run]
-omit = 
-    */.local/*
-    .tox/*
-
-[flake8]
-max-line-length = 88
-select = B, E, F, W, B9, ISC
-ignore = E203, E501, W503, B905, B907
\ No newline at end of file
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 4d41edd1..00000000
--- a/tox.ini
+++ /dev/null
@@ -1,32 +0,0 @@
-[tox]
-isolated_build = True
-envlist = 
-    {py37,py38,py39,py310,py311,pypy37,pypy38,pypy39}-{autoescape,unsafe}
-    typing
-    lint
-
-[testenv]
-deps = 
-    pytest
-    mock
-    autoescape: markupsafe
-commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
-
-[testenv:typing]
-deps = 
-    mypy
-    types-python-dateutil
-    types-MarkupSafe
-commands = mypy
-
-[testenv:lint]
-deps = 
-    black
-    flake8
-    flake8-bugbear
-    mock
-    pylint
-commands = 
-    python -m pylint -rn tests/ liquid/
-    python -m flake8 tests/ liquid/
-    python -m black tests/ liquid/
\ No newline at end of file
diff --git a/tox_cov.ini b/tox_cov.ini
deleted file mode 100644
index f86a0fe0..00000000
--- a/tox_cov.ini
+++ /dev/null
@@ -1,11 +0,0 @@
-[tox]
-isolated_build = True
-envlist = py310-{autoescape,unsafe}
-
-[testenv]
-deps = 
-    mock
-    coverage
-    typing_extensions
-    autoescape: markupsafe
-commands = python -m coverage run -p -m unittest


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 2da48536..da2aadc9 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -1,33 +1,26 @@
-name: Tests
+name: tests
 on: [push, pull_request]
 
 jobs:
   tests:
-    name: ${{ matrix.name }}
+    name: ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }}
     runs-on: ${{ matrix.os }}
     strategy:
       fail-fast: false
       matrix:
-        include:
-          - {name: Windows, python: '3.10', os: windows-latest, tox: py310}
-          - {name: Mac, python: '3.10', os: macos-latest, tox: py310}
-          - {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311-unsafe}
-          - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310-unsafe}
-          - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39-unsafe}
-          - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38-unsafe}
-          - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37-unsafe}
-          - {name: 'PyPy', python: pypy-3.7, os: ubuntu-latest, tox: pypy37-unsafe}
-          - {name: '3.11-autoescape', python: '3.11', os: ubuntu-latest, tox: py311-autoescape}
-          - {name: '3.10-autoescape', python: '3.10', os: ubuntu-latest, tox: py310-autoescape}
-          - {name: '3.9-autoescape', python: '3.9', os: ubuntu-latest, tox: py39-autoescape}
-          - {name: '3.8-autoescape', python: '3.8', os: ubuntu-latest, tox: py38-autoescape}
-          - {name: '3.7-autoescape', python: '3.7', os: ubuntu-latest, tox: py37-autoescape}
-          - {name: 'PyPy-autoescape', python: pypy-3.7, os: ubuntu-latest, tox: pypy37-autoescape}
+        os: [ubuntu-latest, windows-latest, macos-latest]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
     steps:
       - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
         with:
-          python-version: ${{ matrix.python }}
-          cache: 'pip'
-      - run: pip install tox
-      - run: python -m tox -e ${{ matrix.tox }}
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install --upgrade hatch
+      - name: Test
+        run: |
+          hatch run test
+          hatch run noescape:test
diff --git a/tests/filters/test_array.py b/tests/filters/test_array.py
index 5e8a5f74..5bc0afc9 100644
--- a/tests/filters/test_array.py
+++ b/tests/filters/test_array.py
@@ -1,38 +1,30 @@
 """Test array filter functions."""
-# pylint: disable=too-many-public-methods,too-many-lines
 import unittest
-
 from functools import partial
 from inspect import isclass
-
 from typing import Any
 from typing import Dict
 from typing import Iterable
 from typing import List
 from typing import NamedTuple
 
-
-from liquid.environment import Environment
-from liquid.expression import NIL
-
-
-from liquid.exceptions import Error
-from liquid.exceptions import FilterError
-from liquid.exceptions import FilterArgumentError
-from liquid.exceptions import FilterValueError
-
-from liquid.builtin.filters.array import join
+from liquid.builtin.filters.array import compact
+from liquid.builtin.filters.array import concat
 from liquid.builtin.filters.array import first
+from liquid.builtin.filters.array import join
 from liquid.builtin.filters.array import last
-from liquid.builtin.filters.array import concat
 from liquid.builtin.filters.array import map_
 from liquid.builtin.filters.array import reverse
 from liquid.builtin.filters.array import sort
 from liquid.builtin.filters.array import sort_natural
-from liquid.builtin.filters.array import where
 from liquid.builtin.filters.array import uniq
-from liquid.builtin.filters.array import compact
-
+from liquid.builtin.filters.array import where
+from liquid.environment import Environment
+from liquid.exceptions import Error
+from liquid.exceptions import FilterArgumentError
+from liquid.exceptions import FilterError
+from liquid.exceptions import FilterValueError
+from liquid.expression import NIL
 from liquid.extra.filters.array import index
 from liquid.extra.filters.array import sort_numeric
 
@@ -69,7 +61,6 @@ def _test(self, func, test_cases: Iterable[Case]):
 
     def test_join(self):
         """Test `join` filter function."""
-
         test_cases = [
             Case(
                 description="lists of strings",
@@ -153,7 +144,6 @@ def test_join(self):
 
     def test_first(self):
         """Test `first` filter function."""
-
         test_cases = [
             Case(
                 description="lists of strings",
@@ -203,7 +193,6 @@ def test_first(self):
 
     def test_last(self):
         """Test `last` filter function."""
-
         test_cases = [
             Case(
                 description="lists of strings",
@@ -253,7 +242,6 @@ def test_last(self):
 
     def test_concat(self):
         """Test `concat` filter function."""
-
         test_cases = [
             Case(
                 description="lists of strings",
@@ -317,7 +305,6 @@ def test_concat(self):
 
     def test_map(self):
         """Test `map` filter function."""
-
         test_cases = [
             Case(
                 description="lists of objects",
@@ -381,7 +368,6 @@ def test_map(self):
 
     def test_reverse(self):
         """Test `reverse` filter function."""
-
         test_cases = [
             Case(
                 description="lists of strings",
@@ -431,7 +417,6 @@ def test_reverse(self):
 
     def test_sort(self):
         """Test `sort` filter function."""
-
         test_cases = [
             Case(
                 description="lists of strings",
@@ -502,7 +487,6 @@ def test_sort(self):
 
     def test_sort_natural(self):
         """Test `sort_natural` filter function."""
-
         test_cases = [
             Case(
                 description="lists of strings",
@@ -573,7 +557,6 @@ def test_sort_natural(self):
 
     def test_where(self):
         """Test `where` filter function."""
-
         test_cases = [
             Case(
                 description="lists of object",
@@ -637,7 +620,6 @@ def test_where(self):
 
     def test_uniq(self):
         """Test `uniq` filter function."""
-
         test_cases = [
             Case(
                 description="lists of strings",
@@ -694,7 +676,6 @@ def test_uniq(self):
 
     def test_compact(self):
         """Test `compact` filter function."""
-
         test_cases = [
             Case(
                 description="lists with nil",
diff --git a/tests/filters/test_html.py b/tests/filters/test_html.py
index 1a811c16..6f153fee 100644
--- a/tests/filters/test_html.py
+++ b/tests/filters/test_html.py
@@ -1,9 +1,7 @@
 """Test html filter functions."""
 import unittest
-
 from functools import partial
 from inspect import isclass
-
 from typing import Any
 from typing import Dict
 from typing import Iterable
@@ -11,20 +9,18 @@
 from typing import NamedTuple
 
 try:
-    import markupsafe  # pylint: disable=unused-import # noqa: F401
+    import markupsafe  # noqa: F401
 
     MARKUPSAFE_AVAILABLE = True
 except ImportError:
     MARKUPSAFE_AVAILABLE = False
 
+from liquid import Markup
 from liquid.environment import Environment
 from liquid.exceptions import Error
-
 from liquid.extra.filters.html import script_tag
 from liquid.extra.filters.html import stylesheet_tag
 
-from liquid import Markup
-
 
 class Case(NamedTuple):
     """Table-driven test helper."""
diff --git a/tests/filters/test_json.py b/tests/filters/test_json.py
index d62c151a..ae793f5b 100644
--- a/tests/filters/test_json.py
+++ b/tests/filters/test_json.py
@@ -1,11 +1,9 @@
 """Test cases for the `json` filter function."""
 import unittest
-
 from dataclasses import asdict
 from dataclasses import dataclass
 from dataclasses import is_dataclass
 from inspect import isclass
-
 from typing import Any
 from typing import Dict
 from typing import Iterable
diff --git a/tests/filters/test_math.py b/tests/filters/test_math.py
index 307003fd..23b75a9d 100644
--- a/tests/filters/test_math.py
+++ b/tests/filters/test_math.py
@@ -1,30 +1,25 @@
 """Test math filter functions."""
-# pylint: disable=too-many-public-methods,too-many-lines,missing-class-docstring
 import unittest
-
 from functools import partial
 from inspect import isclass
-
-from typing import NamedTuple
 from typing import Any
-from typing import List
 from typing import Dict
-
-from liquid.environment import Environment
-
-from liquid.exceptions import FilterArgumentError
+from typing import List
+from typing import NamedTuple
 
 from liquid.builtin.filters.math import abs_
-from liquid.builtin.filters.math import at_most
 from liquid.builtin.filters.math import at_least
+from liquid.builtin.filters.math import at_most
 from liquid.builtin.filters.math import ceil
 from liquid.builtin.filters.math import divided_by
 from liquid.builtin.filters.math import floor
 from liquid.builtin.filters.math import minus
+from liquid.builtin.filters.math import modulo
 from liquid.builtin.filters.math import plus
 from liquid.builtin.filters.math import round_
 from liquid.builtin.filters.math import times
-from liquid.builtin.filters.math import modulo
+from liquid.environment import Environment
+from liquid.exceptions import FilterArgumentError
 
 
 class Case(NamedTuple):
@@ -61,7 +56,6 @@ def _test(self, func, test_cases):
 
     def test_abs(self):
         """Test abs filter function."""
-
         test_cases = [
             Case(
                 description="positive integer",
@@ -160,7 +154,6 @@ def test_abs(self):
 
     def test_at_most(self):
         """Test at_most filter function."""
-
         test_cases = [
             Case(
                 description="positive integer < arg",
@@ -252,7 +245,6 @@ def test_at_most(self):
 
     def test_at_least(self):
         """Test at_least filter function."""
-
         test_cases = [
             Case(
                 description="positive integer < arg",
@@ -344,7 +336,6 @@ def test_at_least(self):
 
     def test_ceil(self):
         """Test ceil filter function."""
-
         test_cases = [
             Case(
                 description="positive integer",
@@ -429,7 +420,6 @@ def test_ceil(self):
 
     def test_floor(self):
         """Test floor filter function."""
-
         test_cases = [
             Case(
                 description="positive integer",
@@ -514,7 +504,6 @@ def test_floor(self):
 
     def test_divided_by(self):
         """Test divided_by filter function."""
-
         test_cases = [
             Case(
                 description="integer value and integer arg",
@@ -599,7 +588,6 @@ def test_divided_by(self):
 
     def test_minus(self):
         """Test minus filter function."""
-
         test_cases = [
             Case(
                 description="integer value and integer arg",
@@ -677,7 +665,6 @@ def test_minus(self):
 
     def test_plus(self):
         """Test plus filter function."""
-
         test_cases = [
             Case(
                 description="integer value and integer arg",
@@ -755,7 +742,6 @@ def test_plus(self):
 
     def test_round(self):
         """Test round filter function."""
-
         test_cases = [
             Case(
                 description="float round down",
@@ -826,7 +812,6 @@ def test_round(self):
 
     def test_times(self):
         """Test times filter function."""
-
         test_cases = [
             Case(
                 description="int times int",
@@ -904,7 +889,6 @@ def test_times(self):
 
     def test_modulo(self):
         """Test modulo filter function."""
-
         test_cases = [
             Case(
                 description="integer value and integer arg",
diff --git a/tests/filters/test_misc.py b/tests/filters/test_misc.py
index 2920efef..f0588942 100644
--- a/tests/filters/test_misc.py
+++ b/tests/filters/test_misc.py
@@ -1,29 +1,23 @@
 """Test miscellaneous filter functions."""
-# pylint: disable=too-many-public-methods,too-many-lines,missing-class-docstring
 import datetime
 import decimal
 import platform
 import unittest
-
 from functools import partial
 from inspect import isclass
-
-from typing import NamedTuple
 from typing import Any
-from typing import List
 from typing import Dict
+from typing import List
+from typing import NamedTuple
 
+from liquid.builtin.filters.misc import date
+from liquid.builtin.filters.misc import default
+from liquid.builtin.filters.misc import size
 from liquid.environment import Environment
-
 from liquid.exceptions import Error
 from liquid.exceptions import FilterArgumentError
-
 from liquid.expression import EMPTY
 
-from liquid.builtin.filters.misc import size
-from liquid.builtin.filters.misc import default
-from liquid.builtin.filters.misc import date
-
 
 class Case(NamedTuple):
     description: str
diff --git a/tests/filters/test_string.py b/tests/filters/test_string.py
index d6aa6d0d..7d8532f6 100644
--- a/tests/filters/test_string.py
+++ b/tests/filters/test_string.py
@@ -1,22 +1,18 @@
 """Test cases for string filters."""
-# pylint: disable=too-many-public-methods,too-many-lines,missing-class-docstring
 import unittest
-
 from functools import partial
 from inspect import isclass
-
-from typing import NamedTuple
 from typing import Any
-from typing import List
 from typing import Dict
+from typing import List
+from typing import NamedTuple
 
-from liquid.environment import Environment
-from liquid.exceptions import FilterArgumentError
-from liquid.exceptions import FilterValueError
-from liquid.exceptions import FilterError
-
-from liquid.builtin.filters.string import capitalize
 from liquid.builtin.filters.string import append
+from liquid.builtin.filters.string import base64_decode
+from liquid.builtin.filters.string import base64_encode
+from liquid.builtin.filters.string import base64_url_safe_decode
+from liquid.builtin.filters.string import base64_url_safe_encode
+from liquid.builtin.filters.string import capitalize
 from liquid.builtin.filters.string import downcase
 from liquid.builtin.filters.string import escape
 from liquid.builtin.filters.string import escape_once
@@ -28,21 +24,21 @@
 from liquid.builtin.filters.string import replace
 from liquid.builtin.filters.string import replace_first
 from liquid.builtin.filters.string import replace_last
+from liquid.builtin.filters.string import rstrip
 from liquid.builtin.filters.string import slice_
 from liquid.builtin.filters.string import split
-from liquid.builtin.filters.string import upcase
 from liquid.builtin.filters.string import strip
-from liquid.builtin.filters.string import rstrip
 from liquid.builtin.filters.string import strip_html
 from liquid.builtin.filters.string import strip_newlines
 from liquid.builtin.filters.string import truncate
 from liquid.builtin.filters.string import truncatewords
-from liquid.builtin.filters.string import url_encode
+from liquid.builtin.filters.string import upcase
 from liquid.builtin.filters.string import url_decode
-from liquid.builtin.filters.string import base64_encode
-from liquid.builtin.filters.string import base64_decode
-from liquid.builtin.filters.string import base64_url_safe_encode
-from liquid.builtin.filters.string import base64_url_safe_decode
+from liquid.builtin.filters.string import url_encode
+from liquid.environment import Environment
+from liquid.exceptions import FilterArgumentError
+from liquid.exceptions import FilterError
+from liquid.exceptions import FilterValueError
 
 
 class Case(NamedTuple):
@@ -77,7 +73,6 @@ def _test(self, func, test_cases):
 
     def test_capitalize(self):
         """Test capitalize filter function."""
-
         test_cases = [
             Case(
                 description="lower case string",
@@ -113,7 +108,6 @@ def test_capitalize(self):
 
     def test_append(self):
         """Test append filter function."""
-
         test_cases = [
             Case(
                 description="concat",
@@ -170,7 +164,6 @@ def test_append(self):
 
     def test_downcase(self):
         """Test downcase filter function."""
-
         test_cases = [
             Case(
                 description="make lower case",
@@ -206,7 +199,6 @@ def test_downcase(self):
 
     def test_escape(self):
         """Test escape filter function."""
-
         test_cases = [
             Case(
                 description="make HTML-safe",
@@ -242,7 +234,6 @@ def test_escape(self):
 
     def test_escape_once(self):
         """Test escape_once filter function."""
-
         test_cases = [
             Case(
                 description="make HTML-safe",
@@ -285,7 +276,6 @@ def test_escape_once(self):
 
     def test_lstrip(self):
         """Test lstrip filter function."""
-
         test_cases = [
             Case(
                 description="left padded",
@@ -335,7 +325,6 @@ def test_lstrip(self):
 
     def test_newline_to_br(self):
         """Test newline_to_br filter function."""
-
         test_cases = [
             Case(
                 description="string with newlines",
@@ -385,7 +374,6 @@ def test_newline_to_br(self):
 
     def test_prepend(self):
         """Test prepend filter function."""
-
         test_cases = [
             Case(
                 description="concat",
@@ -442,7 +430,6 @@ def test_prepend(self):
 
     def test_remove(self):
         """Test remove filter function."""
-
         test_cases = [
             Case(
                 description="remove substrings",
@@ -499,7 +486,6 @@ def test_remove(self):
 
     def test_remove_first(self):
         """Test remove_first filter function."""
-
         test_cases = [
             Case(
                 description="remove substrings",
@@ -556,7 +542,6 @@ def test_remove_first(self):
 
     def test_replace(self):
         """Test replace filter function."""
-
         test_cases = [
             Case(
                 description="replace substrings",
@@ -767,7 +752,6 @@ def test_replace_last(self):
 
     def test_slice(self):
         """Test slice filter function."""
-
         test_cases = [
             Case(
                 description="zero",
@@ -901,7 +885,6 @@ def test_slice(self):
 
     def test_split(self):
         """Test split filter function."""
-
         test_cases = [
             Case(
                 description="split string",
@@ -983,7 +966,6 @@ def test_split(self):
 
     def test_upcase(self):
         """Test upcase filter function."""
-
         test_cases = [
             Case(
                 description="make lower case",
@@ -1019,7 +1001,6 @@ def test_upcase(self):
 
     def test_strip(self):
         """Test strip filter function."""
-
         test_cases = [
             Case(
                 description="left padded",
@@ -1069,7 +1050,6 @@ def test_strip(self):
 
     def test_rstrip(self):
         """Test rstrip filter function."""
-
         test_cases = [
             Case(
                 description="left padded",
@@ -1119,7 +1099,6 @@ def test_rstrip(self):
 
     def test_strip_html(self):
         """Test strip_html filter function."""
-
         test_cases = [
             Case(
                 description="some HTML markup",
@@ -1165,7 +1144,6 @@ def test_strip_html(self):
 
     def test_strip_newlines(self):
         """Test strip_newlines filter function."""
-
         test_cases = [
             Case(
                 description="newline and other whitespace",
@@ -1215,7 +1193,6 @@ def test_strip_newlines(self):
 
     def test_truncate(self):
         """Test truncate filter function."""
-
         test_cases = [
             Case(
                 description="default end",
@@ -1307,7 +1284,6 @@ def test_truncate(self):
 
     def test_truncatewords(self):
         """Test truncatewords filter function."""
-
         test_cases = [
             Case(
                 description="default end",
@@ -1441,7 +1417,6 @@ def test_truncatewords(self):
 
     def test_url_encode_html(self):
         """Test url_encode filter function."""
-
         test_cases = [
             Case(
                 description="some special URL characters",
@@ -1477,7 +1452,6 @@ def test_url_encode_html(self):
 
     def test_url_decode_html(self):
         """Test url_decode filter function."""
-
         test_cases = [
             Case(
                 description="some special URL characters",
@@ -1513,7 +1487,6 @@ def test_url_decode_html(self):
 
     def test_base64_encode(self):
         """Test base64_encode filter function."""
-
         test_cases = [
             Case(
                 description="from string",
@@ -1563,7 +1536,6 @@ def test_base64_encode(self):
 
     def test_base64_decode(self):
         """Test base64_decode filter function."""
-
         test_cases = [
             Case(
                 description="from string",
@@ -1613,7 +1585,6 @@ def test_base64_decode(self):
 
     def test_base64_url_safe_encode(self):
         """Test base64_url_safe_encode filter function."""
-
         test_cases = [
             Case(
                 description="from string",
@@ -1663,7 +1634,6 @@ def test_base64_url_safe_encode(self):
 
     def test_base64_url_safe_decode(self):
         """Test base64_url_safe_decode filter function."""
-
         test_cases = [
             Case(
                 description="from string",
diff --git a/tests/mocks/filters/json.py b/tests/mocks/filters/json.py
index b1fa1e04..b4ab8524 100644
--- a/tests/mocks/filters/json.py
+++ b/tests/mocks/filters/json.py
@@ -2,8 +2,8 @@
 
 import json
 
-from liquid.filter import liquid_filter
 from liquid.exceptions import FilterArgumentError
+from liquid.filter import liquid_filter
 
 
 @liquid_filter
diff --git a/tests/mocks/filters/shop.py b/tests/mocks/filters/shop.py
index e35e512b..39d0521d 100644
--- a/tests/mocks/filters/shop.py
+++ b/tests/mocks/filters/shop.py
@@ -1,11 +1,8 @@
 """Mick shop related filters."""
-# pylint: disable=missing-function-docstring
+from liquid.exceptions import FilterArgumentError
 from liquid.filter import liquid_filter
 from liquid.filter import string_filter
 
-from liquid.exceptions import FilterArgumentError
-
-
 TRANSLATION_TABLE = {"'": None, '"': None, "(": None, ")": None, "[": None, "]": None}
 
 
diff --git a/tests/mocks/filters/tag.py b/tests/mocks/filters/tag.py
index dc02018d..5052e95b 100644
--- a/tests/mocks/filters/tag.py
+++ b/tests/mocks/filters/tag.py
@@ -1,7 +1,6 @@
 """Mock HTML tag related filter functions."""
-# pylint: disable=missing-function-docstring
-from liquid.filter import string_filter
 from liquid.filter import liquid_filter
+from liquid.filter import string_filter
 from liquid.filter import with_context
 
 
diff --git a/tests/mocks/filters/weight.py b/tests/mocks/filters/weight.py
index 3dc0f628..7de2f9a9 100644
--- a/tests/mocks/filters/weight.py
+++ b/tests/mocks/filters/weight.py
@@ -1,5 +1,4 @@
 """Mock filter functions for displaying values as weights."""
-# pylint: disable=missing-function-docstring
 from liquid.filter import math_filter
 
 
diff --git a/tests/mocks/tags/form_tag.py b/tests/mocks/tags/form_tag.py
index b09c00b4..3f259546 100644
--- a/tests/mocks/tags/form_tag.py
+++ b/tests/mocks/tags/form_tag.py
@@ -1,29 +1,24 @@
 """Tag and node definition for the mock "form" tag."""
 
 import sys
-
 from collections import abc
-
 from typing import Optional
 from typing import TextIO
 
-from liquid.token import Token
-from liquid.token import TOKEN_EXPRESSION
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_EOF
-
-from liquid.tag import Tag
 from liquid import ast
-from liquid.stream import TokenStream
-from liquid.lex import tokenize_identifier
-from liquid.expression import Identifier
 from liquid.context import Context
-
-from liquid.parse import get_parser
+from liquid.expression import Identifier
+from liquid.lex import tokenize_identifier
 from liquid.parse import expect
+from liquid.parse import get_parser
 from liquid.parse import parse_identifier
-
+from liquid.stream import TokenStream
+from liquid.tag import Tag
+from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
 TAG_FORM = sys.intern("form")
 TAG_ENDFORM = sys.intern("endform")
diff --git a/tests/mocks/tags/paginate_tag.py b/tests/mocks/tags/paginate_tag.py
index 2afd3db9..2457fcbd 100644
--- a/tests/mocks/tags/paginate_tag.py
+++ b/tests/mocks/tags/paginate_tag.py
@@ -1,36 +1,29 @@
 """Tag and node definition for the mock "paginate" tag."""
-# pylint: disable=missing-function-docstring
 import math
 import sys
-
 from collections import abc
-
 from typing import List
 from typing import Optional
 from typing import TextIO
 
-from liquid.token import Token
-from liquid.token import TOKEN_EXPRESSION
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_BY
-from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_EOF
-
-from liquid.tag import Tag
 from liquid import ast
-from liquid.stream import TokenStream
-from liquid.lex import tokenize_paginate_expression
-from liquid.expression import Identifier
 from liquid.context import Context
-
-from liquid.parse import get_parser
+from liquid.exceptions import LiquidTypeError
+from liquid.expression import Identifier
+from liquid.lex import tokenize_paginate_expression
 from liquid.parse import expect
+from liquid.parse import get_parser
 from liquid.parse import parse_identifier
 from liquid.parse import parse_integer_literal
-
-from liquid.exceptions import LiquidTypeError
-
+from liquid.stream import TokenStream
+from liquid.tag import Tag
+from liquid.token import TOKEN_BY
+from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_INTEGER
+from liquid.token import TOKEN_TAG
+from liquid.token import Token
 
 TAG_PAGINATE = sys.intern("paginate")
 TAG_ENDPAGINATE = sys.intern("endpaginate")
diff --git a/tests/test_analyze_template.py b/tests/test_analyze_template.py
index f7cc37af..3b3e7c71 100644
--- a/tests/test_analyze_template.py
+++ b/tests/test_analyze_template.py
@@ -1,48 +1,39 @@
 """Test cases for statically counting a template using `BoundTemplate.analyze`."""
-# pylint: disable=too-many-lines
 import asyncio
-
 from typing import List
 from typing import Optional
 from typing import TextIO
-
 from unittest import TestCase
 
-from liquid import Environment
 from liquid import DictLoader
+from liquid import Environment
 from liquid import Template
-
 from liquid.ast import ChildNode
 from liquid.ast import Node
-
 from liquid.context import Context
 from liquid.exceptions import TemplateInheritanceError
 from liquid.exceptions import TemplateTraversalError
 from liquid.expression import Expression
-
+from liquid.extra import WithTag
 from liquid.extra import add_inheritance_tags
 from liquid.extra import add_inline_expression_tags
-from liquid.extra import WithTag
-
 from liquid.mode import Mode
 from liquid.stream import TokenStream
 from liquid.tag import Tag
-
 from liquid.template import BoundTemplate
 from liquid.template import NameRefs
 from liquid.template import Refs
 from liquid.template import TemplateAnalysis
-
 from liquid.token import Token
 
 
 class MockExpression(Expression):
-    """Mock expression"""
+    """Mock expression."""
 
-    def evaluate(self, context: Context) -> object:
+    def evaluate(self, _: Context) -> object:
         return "mock expression"
 
-    async def evaluate_async(self, context: Context) -> object:
+    async def evaluate_async(self, _: Context) -> object:
         return "mock expression"
 
 
@@ -52,11 +43,11 @@ class MockNode(Node):
     def __init__(self, token: Token) -> None:
         self.tok = token
 
-    def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
+    def render_to_output(self, _: Context, buffer: TextIO) -> Optional[bool]:
         buffer.write("mock node")
 
     async def render_to_output_async(
-        self, context: Context, buffer: TextIO
+        self, _: Context, buffer: TextIO
     ) -> Optional[bool]:
         buffer.write("mock node")
 
@@ -79,17 +70,15 @@ def children(self) -> List[ChildNode]:
 
 
 class MockChildTag(MockTag):
-    """Mock tag"""
+    """Mock tag."""
 
     def parse(self, stream: TokenStream) -> Node:
         return MockChildNode(stream.current)
 
 
-# pylint: disable=too-many-public-methods
 class AnalyzeTemplateTestCase(TestCase):
     """Test that we can count a template's variables, tags and filters."""
 
-    # pylint: disable=too-many-arguments
     def _test(
         self,
         template: BoundTemplate,
@@ -1183,7 +1172,10 @@ def test_analyze_inheritance_chain(self):
                     "{% block content %}{{ x | downcase }}{% endblock %}"
                     "{% block foo %}{% assign z = 7 %}{% endblock %}"
                 ),
-                "some": "{% extends 'other' %}{{ y | append: x }}{% block foo %}{% endblock %}",
+                "some": (
+                    "{% extends 'other' %}{{ y | append: x }}"
+                    "{% block foo %}{% endblock %}"
+                ),
             }
         )
         env = Environment(loader=loader)
diff --git a/tests/test_analyze_template_tags.py b/tests/test_analyze_template_tags.py
index 7ceda518..a3e950d5 100644
--- a/tests/test_analyze_template_tags.py
+++ b/tests/test_analyze_template_tags.py
@@ -5,8 +5,8 @@
 from typing import List
 from unittest import TestCase
 
-from liquid import Environment
 from liquid import DictLoader
+from liquid import Environment
 from liquid.analyze_tags import TagAnalysis
 from liquid.analyze_tags import TagMap
 from liquid.context import Context
diff --git a/tests/test_async.py b/tests/test_async.py
index d184976a..e739ca8f 100644
--- a/tests/test_async.py
+++ b/tests/test_async.py
@@ -1,28 +1,23 @@
 """Test Python Liquid's async API."""
-# pylint: disable=missing-class-docstring
 import asyncio
 import tempfile
 import time
 import unittest
-
 from collections import abc
 from pathlib import Path
-
-from typing import NamedTuple
 from typing import Dict
+from typing import NamedTuple
 
 # assert_awaited* were new in Python 3.8, so we're using the backport.
 from mock import patch
 
-from liquid import Template
 from liquid import Environment
 from liquid import FileSystemLoader
-
-from liquid.loaders import TemplateSource
+from liquid import Template
+from liquid.exceptions import TemplateNotFound
 from liquid.loaders import ChoiceLoader
 from liquid.loaders import DictLoader
-
-from liquid.exceptions import TemplateNotFound
+from liquid.loaders import TemplateSource
 from liquid.template import BoundTemplate
 
 
@@ -165,7 +160,7 @@ async def coro(template: BoundTemplate):
             self.assertEqual(result, "1-2-3-")
 
     def test_include_async(self):
-        """Test that included templates are rendered asynchronously"""
+        """Test that included templates are rendered asynchronously."""
         test_cases = [
             Case(
                 description="simple include",
@@ -251,26 +246,25 @@ async def coro(template: BoundTemplate):
             return await template.render_async()
 
         for case in test_cases:
-            with self.subTest(msg=case.description):
-                with patch(
-                    "liquid.loaders.DictLoader.get_source_async", autospec=True
-                ) as source:
-                    source.side_effect = [
-                        TemplateSource(
-                            "{% for x in (1..3) %}{{x}}-{% endfor %}",
-                            "foo",
-                            None,
-                        )
-                    ]
+            with self.subTest(msg=case.description), patch(
+                "liquid.loaders.DictLoader.get_source_async", autospec=True
+            ) as source:
+                source.side_effect = [
+                    TemplateSource(
+                        "{% for x in (1..3) %}{{x}}-{% endfor %}",
+                        "foo",
+                        None,
+                    )
+                ]
 
-                    env = Environment()
-                    template = env.from_string(case.template, globals=case.context)
+                env = Environment()
+                template = env.from_string(case.template, globals=case.context)
 
-                    result = asyncio.run(coro(template))
+                result = asyncio.run(coro(template))
 
-                    source.assert_awaited_with(env.loader, env=env, template_name="foo")
-                    self.assertEqual(source.call_count, case.calls)
-                    self.assertEqual(result, case.expect)
+                source.assert_awaited_with(env.loader, env=env, template_name="foo")
+                self.assertEqual(source.call_count, case.calls)
+                self.assertEqual(result, case.expect)
 
 
 class MockAsyncDrop(abc.Mapping):
@@ -281,24 +275,24 @@ def __init__(self, val):
         self.call_count = 0
         self.await_count = 0
 
-    def __len__(self):
+    def __len__(self):  # pragma: no cover
         return 1
 
-    def __iter__(self):
+    def __iter__(self):  # pragma: no cover
         return iter([self.key])
 
     def __getitem__(self, k):
         self.call_count += 1
         if k == self.key:
             return self.val
-        raise KeyError(k)
+        raise KeyError(k)  # pragma: no cover
 
     async def __getitem_async__(self, k):
         # Do IO here
         self.await_count += 1
         if k == self.key:
             return self.val
-        raise KeyError(k)
+        raise KeyError(k)  # pragma: no cover
 
 
 class AsyncDropTestCase(unittest.TestCase):
diff --git a/tests/test_autoescape.py b/tests/test_autoescape.py
index 408f8b9d..c660034c 100644
--- a/tests/test_autoescape.py
+++ b/tests/test_autoescape.py
@@ -1,11 +1,9 @@
 """HTML escape test cases."""
-# pylint: disable=missing-class-docstring
-from unittest import TestCase
-from unittest import skipIf
-
-from typing import NamedTuple
 from typing import Dict
 from typing import List
+from typing import NamedTuple
+from unittest import TestCase
+from unittest import skipIf
 
 try:
     import markupsafe
@@ -784,7 +782,9 @@ def test_liquid_tag(self):
                 self.assertEqual(result, case.expect)
 
 
-@skipIf(markupsafe is not None, "this tests exceptions raise by the lack of markupsafe")
+@skipIf(
+    markupsafe is not None, "this tests exceptions raised by the lack of markupsafe"
+)
 class DummyMarkupSafeTestCase(TestCase):
     def test_dummy_escape(self):
         """Test that the dummy definition of escape raises an exception."""
diff --git a/tests/test_compat_context.py b/tests/test_compat_context.py
index 3040484b..e932f067 100644
--- a/tests/test_compat_context.py
+++ b/tests/test_compat_context.py
@@ -6,7 +6,6 @@
 from liquid import Environment
 from liquid import FutureContext
 from liquid import is_undefined
-
 from liquid.future import Environment as RubyEnvironment
 
 
diff --git a/tests/test_context.py b/tests/test_context.py
index be298a5d..3a479524 100644
--- a/tests/test_context.py
+++ b/tests/test_context.py
@@ -1,19 +1,16 @@
 """Bad context test cases."""
 
-from unittest import TestCase
-
 from typing import NamedTuple
 from typing import Type
+from unittest import TestCase
 
-from liquid.context import builtin
-from liquid.context import get_item
 from liquid.context import UNDEFINED
 from liquid.context import ReadOnlyChainMap
+from liquid.context import builtin
+from liquid.context import get_item
 from liquid.environment import Environment
-
 from liquid.exceptions import LiquidTypeError
 from liquid.exceptions import lookup_warning
-
 from liquid.mode import Mode
 
 
@@ -31,7 +28,7 @@ class BadContextTemplateTestCase(TestCase):
     """Bad context test case."""
 
     def _test(self, test_cases, mode=Mode.STRICT):
-        """Helper method for running lists of `Case`s"""
+        """Helper method for running lists of `Case`s."""
         env = Environment()
         env.mode = mode
 
diff --git a/tests/test_contextual_analysis.py b/tests/test_contextual_analysis.py
index 9be0b560..e0bf37eb 100644
--- a/tests/test_contextual_analysis.py
+++ b/tests/test_contextual_analysis.py
@@ -2,14 +2,13 @@
 
 import asyncio
 import unittest
-
-from typing import Dict, Optional
+from typing import Dict
+from typing import Optional
 
 from liquid import Context
 from liquid import DictLoader
 from liquid import Environment
 from liquid import Template
-
 from liquid.filter import with_context
 from liquid.template import BoundTemplate
 
diff --git a/tests/test_custom_template_class.py b/tests/test_custom_template_class.py
index 8acad8e5..fc1e256d 100644
--- a/tests/test_custom_template_class.py
+++ b/tests/test_custom_template_class.py
@@ -1,17 +1,14 @@
 """Test Template and Context subclassing."""
 import io
 import unittest
-
 from typing import Any
-from typing import Optional
 from typing import List
+from typing import Optional
 
 from liquid import Context
 from liquid import Environment
-
 from liquid.context import ContextPath
 from liquid.context import Namespace
-
 from liquid.template import BoundTemplate
 
 
@@ -22,7 +19,7 @@ def test_bound_template_subclass(self):
         """Test that we can use a subclass of BoundTemplate with an Environment."""
 
         class CustomTemplate(BoundTemplate):
-            """Mock template subclass"""
+            """Mock template subclass."""
 
         env = Environment()
         env.template_class = CustomTemplate
@@ -33,14 +30,13 @@ class CustomTemplate(BoundTemplate):
     def test_context_subclass(self):
         """Test that we can use a subclass of Context with an Environment."""
 
-        # pylint: disable=redefined-builtin
         class CustomContext(Context):
-            """Mock context subclass"""
+            """Mock context subclass."""
 
             def __init__(
                 self,
                 env: Environment,
-                globals: Optional[Namespace] = None,
+                globals: Optional[Namespace] = None,  # noqa: A002
                 disabled_tags: Optional[List[str]] = None,
                 copy_depth: int = 0,
             ):
@@ -52,7 +48,7 @@ def assign(self, key: str, val: Any) -> None:
                 return super().assign(key, val)
 
         class CustomTemplate(BoundTemplate):
-            """Mock template subclass"""
+            """Mock template subclass."""
 
             context_class = CustomContext
 
@@ -70,18 +66,16 @@ class CustomTemplate(BoundTemplate):
         self.assertEqual(ctx.assign_counter, 1)
 
     def test_capture_variables_from_context_subclass(self):
-        """Test that we can capture a template's used variables from a Context subclass."""
-
+        """Test that we can capture a template's variables from a Context subclass."""
         _missing = object()
 
-        # pylint: disable=redefined-builtin
         class CustomContext(Context):
-            """Mock context subclass"""
+            """Mock context subclass."""
 
             def __init__(
                 self,
                 env: Environment,
-                globals: Optional[Namespace] = None,
+                globals: Optional[Namespace] = None,  # noqa: A002
                 disabled_tags: Optional[List[str]] = None,
                 copy_depth: int = 0,
             ):
@@ -109,7 +103,7 @@ def _count_reference(self, path: ContextPath) -> None:
                     self.references.append(".".join(str(p) for p in path))
 
         class CustomTemplate(BoundTemplate):
-            """Mock template subclass"""
+            """Mock template subclass."""
 
             context_class = CustomContext
 
diff --git a/tests/test_drop.py b/tests/test_drop.py
index d019c1f5..e96e6bd7 100644
--- a/tests/test_drop.py
+++ b/tests/test_drop.py
@@ -1,7 +1,5 @@
 """Test the "drop" API."""
-# pylint: disable=missing-class-docstring
 import unittest
-
 from typing import Generic
 from typing import NamedTuple
 from typing import TypeVar
diff --git a/tests/test_eval_boolean_expression.py b/tests/test_eval_boolean_expression.py
index b3e63e15..17b326b0 100644
--- a/tests/test_eval_boolean_expression.py
+++ b/tests/test_eval_boolean_expression.py
@@ -1,14 +1,11 @@
-# pylint: disable=missing-class-docstring missing-module-docstring
 import unittest
 from decimal import Decimal
-
 from typing import Any
 from typing import Mapping
 from typing import NamedTuple
 
 from liquid import Context
 from liquid import Environment
-
 from liquid.expressions import parse_boolean_expression
 from liquid.expressions import parse_boolean_expression_with_parens
 
diff --git a/tests/test_eval_filtered_expression.py b/tests/test_eval_filtered_expression.py
index 6abede7d..8969140e 100644
--- a/tests/test_eval_filtered_expression.py
+++ b/tests/test_eval_filtered_expression.py
@@ -1,18 +1,14 @@
-# pylint: disable=missing-class-docstring missing-module-docstring
 import unittest
-
 from typing import Any
 from typing import Mapping
 from typing import NamedTuple
 
 from liquid import Context
 from liquid import Environment
-
 from liquid.context import Undefined
-
-from liquid.expressions import parse_filtered_expression
 from liquid.expressions import parse_conditional_expression
 from liquid.expressions import parse_conditional_expression_with_parens
+from liquid.expressions import parse_filtered_expression
 
 
 class Case(NamedTuple):
diff --git a/tests/test_eval_loop_expression.py b/tests/test_eval_loop_expression.py
index fbcffe34..6ad6dadd 100644
--- a/tests/test_eval_loop_expression.py
+++ b/tests/test_eval_loop_expression.py
@@ -1,6 +1,5 @@
 """Test cases for parsing loop-style expressions."""
 import unittest
-
 from typing import Any
 from typing import Mapping
 from typing import NamedTuple
diff --git a/tests/test_evaluate_expression.py b/tests/test_evaluate_expression.py
index ef772434..87c7dd4f 100644
--- a/tests/test_evaluate_expression.py
+++ b/tests/test_evaluate_expression.py
@@ -1,32 +1,29 @@
 """Liquid expression evaluator test cases."""
 import unittest
 from decimal import Decimal
-
 from typing import Any
 from typing import Mapping
 from typing import NamedTuple
 
-from liquid.environment import Environment
 from liquid.context import Context
-from liquid.stream import TokenStream
+from liquid.environment import Environment
 
 # Note: These are test cases for the legacy expression implementation.
 # See `test_eval_*_expression.py` for expression evaluation test cases
 # as used by all built-in tags.
-
-from liquid.lex import tokenize_filtered_expression
+from liquid.lex import tokenize_assignment_expression
 from liquid.lex import tokenize_boolean_expression
+from liquid.lex import tokenize_filtered_expression
 from liquid.lex import tokenize_loop_expression
-from liquid.lex import tokenize_assignment_expression
-
-from liquid.parse import parse_filtered_expression
-from liquid.parse import parse_boolean_expression
 from liquid.parse import parse_assignment_expression
+from liquid.parse import parse_boolean_expression
+from liquid.parse import parse_filtered_expression
 from liquid.parse import parse_loop_expression
+from liquid.stream import TokenStream
 
 
 class Case(NamedTuple):
-    """Subtest helper"""
+    """Subtest helper."""
 
     description: str
     context: Mapping
@@ -632,7 +629,6 @@ def test_eval_boolean_expression(self):
 
     def test_eval_assignment_expression(self):
         """Test that we can evaluate assignment expressions."""
-
         test_cases = [
             Case(
                 description="assign a string literal",
@@ -680,7 +676,6 @@ def test_eval_assignment_expression(self):
 
     def test_eval_loop_expression(self):
         """Test that we can evaluate loop expressions."""
-
         test_cases = [
             Case(
                 description="simple range loop",
@@ -745,7 +740,6 @@ def test_eval_loop_expression(self):
 
     def test_eval_continue_loop_expression(self):
         """Test that we can evaluate loop expressions that use a continue offset."""
-
         env = Environment()
         context = Context(env, {"array": [1, 2, 3, 4, 5, 6]})
 
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index b7c4e7e3..656c0e15 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -1,12 +1,9 @@
 """Liquid exception test cases."""
-# pylint: disable=missing-class-docstring
-
 from pathlib import Path
 from unittest import TestCase
 
 from liquid import Environment
 from liquid import Mode
-
 from liquid.exceptions import Error
 from liquid.exceptions import LiquidSyntaxError
 
@@ -31,7 +28,7 @@ def test_base_error_message(self):
     def test_base_error_no_message(self):
         """Test that the base error can not include a message."""
         try:
-            raise Error()
+            raise Error
         except Error as err:
             self.assertEqual(err.message, None)
 
diff --git a/tests/test_expression_cache.py b/tests/test_expression_cache.py
index a556d19c..0dbec6ff 100644
--- a/tests/test_expression_cache.py
+++ b/tests/test_expression_cache.py
@@ -1,11 +1,8 @@
 """Expression caching test cases."""
-# pylint: disable=missing-class-docstring
-
 from unittest import TestCase
 
 from liquid import Environment
 from liquid import Template
-
 from liquid.builtin.tags.if_tag import IfNode
 
 
diff --git a/tests/test_expression_token_stream.py b/tests/test_expression_token_stream.py
index 292c09e6..6bd68b81 100644
--- a/tests/test_expression_token_stream.py
+++ b/tests/test_expression_token_stream.py
@@ -1,20 +1,17 @@
 """Token stream test cases."""
-from unittest import TestCase
-
 from typing import List
-
-from liquid.token import TOKEN_STRING
-from liquid.token import TOKEN_PIPE
-from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_COLON
-from liquid.token import TOKEN_COMMA
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_EOF
+from unittest import TestCase
 
 from liquid.exceptions import LiquidSyntaxError
-
 from liquid.expressions import Token
 from liquid.expressions import TokenStream
+from liquid.token import TOKEN_COLON
+from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_EOF
+from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_INTEGER
+from liquid.token import TOKEN_PIPE
+from liquid.token import TOKEN_STRING
 
 
 class ExpresssionTokenStreamTestCase(TestCase):
diff --git a/tests/test_expressions.py b/tests/test_expressions.py
index 8f9ad179..07863ea7 100644
--- a/tests/test_expressions.py
+++ b/tests/test_expressions.py
@@ -1,35 +1,31 @@
 """Test liquid expressions."""
-# pylint: disable=missing-class-docstring
 from unittest import TestCase
 
 from liquid import Context
 from liquid import Environment
-
-from liquid.expression import NIL
+from liquid.exceptions import LiquidTypeError
 from liquid.expression import BLANK
 from liquid.expression import CONTINUE
-from liquid.expression import StringLiteral
-from liquid.expression import RangeLiteral
-from liquid.expression import IntegerLiteral
+from liquid.expression import NIL
 from liquid.expression import Identifier
 from liquid.expression import IdentifierPathElement
+from liquid.expression import IntegerLiteral
 from liquid.expression import PrefixExpression
-
-from liquid.exceptions import LiquidTypeError
+from liquid.expression import RangeLiteral
+from liquid.expression import StringLiteral
 
 
 class CompareExpressionTestCase(TestCase):
     def test_compare_nil_to_none(self):
         """Test that nil is equal to None."""
-        self.assertTrue(NIL.__eq__(None))  # pylint: disable=unnecessary-dunder-call
+        self.assertTrue(NIL.__eq__(None))
 
     def test_compare_blank_to_blank(self):
         """Test that blank is equal to blank."""
-        self.assertTrue(BLANK.__eq__(BLANK))  # pylint: disable=unnecessary-dunder-call
+        self.assertTrue(BLANK.__eq__(BLANK))
 
     def test_compare_continue(self):
         """Test that continue is only equal to itself."""
-        # pylint: disable=unnecessary-dunder-call
         self.assertFalse(CONTINUE.__eq__(object()))
 
 
diff --git a/tests/test_filter.py b/tests/test_filter.py
index 1323e3a8..4dac0a44 100644
--- a/tests/test_filter.py
+++ b/tests/test_filter.py
@@ -1,16 +1,13 @@
 """Test filter decorators and helpers."""
-# pylint: disable=missing-class-docstring
 from unittest import TestCase
 
-from liquid import Environment
 from liquid import Context
-
+from liquid import Environment
+from liquid.exceptions import FilterArgumentError
+from liquid.filter import int_arg
 from liquid.filter import liquid_filter
-from liquid.filter import with_context
 from liquid.filter import num_arg
-from liquid.filter import int_arg
-
-from liquid.exceptions import FilterArgumentError
+from liquid.filter import with_context
 
 
 @liquid_filter
diff --git a/tests/test_find_comments.py b/tests/test_find_comments.py
index a674ea60..2c4a4a60 100644
--- a/tests/test_find_comments.py
+++ b/tests/test_find_comments.py
@@ -1,20 +1,20 @@
-"""Test that we can traverse a template's syntax tree to find comments"""
-# pylint: disable=missing-class-docstring missing-function-docstring
-
+"""Test that we can traverse a template's syntax tree to find comments."""
 import unittest
-
+from typing import TYPE_CHECKING
 from typing import Iterator
 from typing import Tuple
 
 from liquid import Template
-from liquid.ast import Node
 from liquid.builtin.tags.comment_tag import CommentNode
 from liquid.template import BoundTemplate
 
+if TYPE_CHECKING:
+    from liquid.ast import Node
+
 
 class FindCommentsTestCase(unittest.TestCase):
     def _find_comment_text(self, template: BoundTemplate) -> Iterator[Tuple[int, str]]:
-        def visit(node: Node) -> Iterator[Tuple[int, str]]:
+        def visit(node: "Node") -> Iterator[Tuple[int, str]]:
             if isinstance(node, CommentNode) and node.text is not None:
                 yield (node.tok.linenum, node.text)
             for child in node.children():
diff --git a/tests/test_large_str_to_int.py b/tests/test_large_str_to_int.py
index 107a98a9..f39aa0fa 100644
--- a/tests/test_large_str_to_int.py
+++ b/tests/test_large_str_to_int.py
@@ -1,14 +1,11 @@
-# pylint: disable=missing-class-docstring missing-module-docstring
 import os
 import unittest
 
 from mock import patch
 
-from liquid import Template
 from liquid import Mode
-
+from liquid import Template
 from liquid.exceptions import LiquidValueError
-
 from liquid.limits import MAX_STR_INT
 from liquid.limits import _init_int_max_str_digits
 
@@ -53,5 +50,5 @@ def test_set_liquid_max(self):
 
     @patch.dict(os.environ, {"LIQUIDINTMAXSTRDIGITS": "0"})
     def test_zero_is_ok(self):
-        """Test that zero is OK"""
+        """Test that zero is OK."""
         self.assertEqual(_init_int_max_str_digits(), 0)
diff --git a/tests/test_lex_boolean_expression.py b/tests/test_lex_boolean_expression.py
index 47c8f652..7a696371 100644
--- a/tests/test_lex_boolean_expression.py
+++ b/tests/test_lex_boolean_expression.py
@@ -1,15 +1,12 @@
 """Test Liquid boolean expression tokenization."""
 
 import unittest
-
 from typing import Any
 from typing import NamedTuple
 
 from liquid.exceptions import LiquidSyntaxError
-
 from liquid.expressions.boolean import tokenize
 from liquid.expressions.boolean import tokenize_with_parens
-
 from liquid.token import TOKEN_AND
 from liquid.token import TOKEN_BLANK
 from liquid.token import TOKEN_CONTAINS
@@ -330,14 +327,12 @@ class LexBooleanExpressionErrorsTestCase(unittest.TestCase):
     def test_lex_illegal_expression(self) -> None:
         """Test that we raise a syntax error upon illegal characters."""
         for case in self.test_cases:
-            with self.subTest(msg=case.description):
-                with self.assertRaises(case.expect):
-                    list(tokenize(case.source))
+            with self.subTest(msg=case.description), self.assertRaises(case.expect):
+                list(tokenize(case.source))
 
     def test_lex_illegal_expression_with_parens(self) -> None:
         """Test that we raise a syntax error upon illegal characters when using the
         non-standard lexer."""
         for case in self.test_cases:
-            with self.subTest(msg=case.description):
-                with self.assertRaises(case.expect):
-                    list(tokenize_with_parens(case.source))
+            with self.subTest(msg=case.description), self.assertRaises(case.expect):
+                list(tokenize_with_parens(case.source))
diff --git a/tests/test_lex_filtered_expression.py b/tests/test_lex_filtered_expression.py
index 36deac18..5acb3325 100644
--- a/tests/test_lex_filtered_expression.py
+++ b/tests/test_lex_filtered_expression.py
@@ -5,38 +5,36 @@
 """
 
 import unittest
-
 from typing import Any
 from typing import NamedTuple
 
-from liquid.expressions.filtered import tokenize
 from liquid.expressions.conditional import tokenize as tokenize_conditional
 from liquid.expressions.conditional import (
     tokenize_with_parens as tokenize_conditional_with_parens,
 )
-
+from liquid.expressions.filtered import tokenize
+from liquid.token import TOKEN_AND
 from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_COMMA
 from liquid.token import TOKEN_DOT
 from liquid.token import TOKEN_DPIPE
+from liquid.token import TOKEN_ELSE
+from liquid.token import TOKEN_FALSE
 from liquid.token import TOKEN_FLOAT
 from liquid.token import TOKEN_IDENTIFIER
 from liquid.token import TOKEN_IDENTINDEX
+from liquid.token import TOKEN_IF
 from liquid.token import TOKEN_INTEGER
 from liquid.token import TOKEN_LBRACKET
 from liquid.token import TOKEN_LPAREN
+from liquid.token import TOKEN_LT
+from liquid.token import TOKEN_NOT
 from liquid.token import TOKEN_PIPE
 from liquid.token import TOKEN_RANGE
 from liquid.token import TOKEN_RBRACKET
 from liquid.token import TOKEN_RPAREN
 from liquid.token import TOKEN_STRING
-from liquid.token import TOKEN_IF
 from liquid.token import TOKEN_TRUE
-from liquid.token import TOKEN_ELSE
-from liquid.token import TOKEN_NOT
-from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_AND
-from liquid.token import TOKEN_LT
 from liquid.token import Token
 
 
diff --git a/tests/test_lex_include_expression.py b/tests/test_lex_include_expression.py
index ba734748..ab9c8038 100644
--- a/tests/test_lex_include_expression.py
+++ b/tests/test_lex_include_expression.py
@@ -10,7 +10,6 @@
 from typing import NamedTuple
 
 from liquid.expressions.include import tokenize
-
 from liquid.token import TOKEN_AS
 from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_COMMA
diff --git a/tests/test_lex_loop_expression.py b/tests/test_lex_loop_expression.py
index 8be3a352..551b5305 100644
--- a/tests/test_lex_loop_expression.py
+++ b/tests/test_lex_loop_expression.py
@@ -9,21 +9,20 @@
 from typing import NamedTuple
 
 from liquid.expressions.loop import tokenize
-
-from liquid.token import TOKEN_IN
 from liquid.token import TOKEN_COLON
-from liquid.token import TOKEN_FLOAT
+from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_CONTINUE
 from liquid.token import TOKEN_DOT
-from liquid.token import TOKEN_REVERSED
+from liquid.token import TOKEN_FLOAT
 from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_LIMIT
+from liquid.token import TOKEN_IN
 from liquid.token import TOKEN_INTEGER
+from liquid.token import TOKEN_LIMIT
 from liquid.token import TOKEN_LPAREN
+from liquid.token import TOKEN_OFFSET
 from liquid.token import TOKEN_RANGE
+from liquid.token import TOKEN_REVERSED
 from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_OFFSET
-from liquid.token import TOKEN_CONTINUE
-from liquid.token import TOKEN_COMMA
 from liquid.token import Token
 
 
diff --git a/tests/test_lexer.py b/tests/test_lexer.py
index a281fd5c..715c96a3 100644
--- a/tests/test_lexer.py
+++ b/tests/test_lexer.py
@@ -1,65 +1,62 @@
 """Test tokenizing of liquid templates and expressions."""
-# pylint: disable=too-many-lines,missing-class-docstring
+from typing import Any
+from typing import NamedTuple
 from unittest import TestCase
-from typing import NamedTuple, Any
 
 from liquid.environment import Environment
-from liquid.stream import TokenStream
-
 from liquid.exceptions import LiquidSyntaxError
 
-from liquid.lex import tokenize_liquid_expression
-
 # Note: These are the legacy expression lexers.
 # See test_lex_*_expression.py for current, built-in lexer test cases.
-from liquid.lex import tokenize_loop_expression
-from liquid.lex import tokenize_filtered_expression
+from liquid.lex import get_lexer
 from liquid.lex import tokenize_boolean_expression
+from liquid.lex import tokenize_filtered_expression
 from liquid.lex import tokenize_include_expression
-from liquid.lex import get_lexer
-
-from liquid.token import Token
-from liquid.token import TOKEN_TAG
-from liquid.token import TOKEN_STATEMENT
-from liquid.token import TOKEN_LITERAL
-from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_STRING
-from liquid.token import TOKEN_INTEGER
-from liquid.token import TOKEN_FLOAT
-from liquid.token import TOKEN_EMPTY
-from liquid.token import TOKEN_NIL
-from liquid.token import TOKEN_NULL
+from liquid.lex import tokenize_liquid_expression
+from liquid.lex import tokenize_loop_expression
+from liquid.stream import TokenStream
+from liquid.token import TOKEN_AND
+from liquid.token import TOKEN_AS
 from liquid.token import TOKEN_BLANK
-from liquid.token import TOKEN_NEGATIVE
-from liquid.token import TOKEN_TRUE
-from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_CONTAINS
-from liquid.token import TOKEN_IN
-from liquid.token import TOKEN_LPAREN
-from liquid.token import TOKEN_RPAREN
-from liquid.token import TOKEN_RANGE
-from liquid.token import TOKEN_LIMIT
-from liquid.token import TOKEN_OFFSET
-from liquid.token import TOKEN_REVERSED
-from liquid.token import TOKEN_CONTINUE
-from liquid.token import TOKEN_PIPE
 from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_COMMA
+from liquid.token import TOKEN_CONTAINS
+from liquid.token import TOKEN_CONTINUE
 from liquid.token import TOKEN_DOT
-from liquid.token import TOKEN_LBRACKET
-from liquid.token import TOKEN_RBRACKET
-from liquid.token import TOKEN_AND
-from liquid.token import TOKEN_OR
+from liquid.token import TOKEN_EMPTY
 from liquid.token import TOKEN_EQ
-from liquid.token import TOKEN_NE
-from liquid.token import TOKEN_LG
+from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_FALSE
+from liquid.token import TOKEN_FLOAT
+from liquid.token import TOKEN_FOR
+from liquid.token import TOKEN_GE
 from liquid.token import TOKEN_GT
+from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_IN
+from liquid.token import TOKEN_INTEGER
+from liquid.token import TOKEN_LBRACKET
 from liquid.token import TOKEN_LE
-from liquid.token import TOKEN_GE
-from liquid.token import TOKEN_EXPRESSION
+from liquid.token import TOKEN_LG
+from liquid.token import TOKEN_LIMIT
+from liquid.token import TOKEN_LITERAL
+from liquid.token import TOKEN_LPAREN
+from liquid.token import TOKEN_NE
+from liquid.token import TOKEN_NEGATIVE
+from liquid.token import TOKEN_NIL
+from liquid.token import TOKEN_NULL
+from liquid.token import TOKEN_OFFSET
+from liquid.token import TOKEN_OR
+from liquid.token import TOKEN_PIPE
+from liquid.token import TOKEN_RANGE
+from liquid.token import TOKEN_RBRACKET
+from liquid.token import TOKEN_REVERSED
+from liquid.token import TOKEN_RPAREN
+from liquid.token import TOKEN_STATEMENT
+from liquid.token import TOKEN_STRING
+from liquid.token import TOKEN_TAG
+from liquid.token import TOKEN_TRUE
 from liquid.token import TOKEN_WITH
-from liquid.token import TOKEN_AS
-from liquid.token import TOKEN_FOR
+from liquid.token import Token
 
 
 class Case(NamedTuple):
@@ -91,7 +88,6 @@ def _test(self, test_cases):
 
     def test_lex_template(self):
         """Test that the lexer can tokenize literals, objects and tags."""
-
         test_cases = [
             Case(
                 "simple template literal",
@@ -295,7 +291,6 @@ def test_lex_template(self):
     def test_lex_liquid_expression(self):
         """Test that the liquid expression lexer can tokenize line delimited
         expressions."""
-
         test_cases = [
             Case(
                 "for loop",
@@ -408,7 +403,6 @@ def test_lex_liquid_expression(self):
 
     def test_lex_errors(self):
         """Test that the lexer does not fall over at common errors."""
-
         test_cases = [
             Case(
                 description="no close statement bracket",
diff --git a/tests/test_load_template.py b/tests/test_load_template.py
index c0d9471f..78aa49fa 100644
--- a/tests/test_load_template.py
+++ b/tests/test_load_template.py
@@ -1,28 +1,23 @@
 """Template loader test cases."""
-# pylint: disable=missing-class-docstring
 import asyncio
 import pickle
 import tempfile
 import time
 import unittest
-
 from pathlib import Path
-
 from typing import Dict
 
-from liquid import Environment
 from liquid import Context
-from liquid.template import BoundTemplate
-from liquid.template import AwareBoundTemplate
-
+from liquid import Environment
+from liquid.exceptions import TemplateNotFound
 from liquid.loaders import BaseLoader
-from liquid.loaders import FileSystemLoader
-from liquid.loaders import FileExtensionLoader
 from liquid.loaders import ChoiceLoader
 from liquid.loaders import DictLoader
+from liquid.loaders import FileExtensionLoader
+from liquid.loaders import FileSystemLoader
 from liquid.loaders import TemplateSource
-
-from liquid.exceptions import TemplateNotFound
+from liquid.template import AwareBoundTemplate
+from liquid.template import BoundTemplate
 
 
 class FileSystemLoaderTestCase(unittest.TestCase):
@@ -209,7 +204,7 @@ def test_multiple_search_paths(self):
         self.assertEqual(template.path, Path("tests/fixtures/subfolder/fallback.html"))
 
     def test_stay_in_search_path(self):
-        """Test that we can't stray above the search path"""
+        """Test that we can't stray above the search path."""
         env = Environment(
             loader=FileSystemLoader(search_path="tests/fixtures/subfolder")
         )
@@ -267,7 +262,7 @@ def test_drop_contains(self):
         self.assertEqual("foo" in template.drop, False)
 
     def test_drop_length(self):
-        """Test that we get the length of a template drop"""
+        """Test that we get the length of a template drop."""
         template = self.env.get_template(name="somename")
         assert isinstance(template, AwareBoundTemplate)
         self.assertEqual(len(template.drop), 3)
@@ -399,7 +394,7 @@ def test_load_template_with_missing_extension(self):
         self.assertIsInstance(template, BoundTemplate)
 
     def test_stay_in_search_path(self):
-        """Test that we can't stray above the search path"""
+        """Test that we can't stray above the search path."""
         env = Environment(
             loader=FileExtensionLoader(
                 search_path="tests/fixtures/subfolder",
@@ -524,7 +519,7 @@ class MockBootstrapLoader(BaseLoader):
     def __init__(self, namespaces: Dict[str, Dict[str, str]]):
         self.namespaces = namespaces
 
-    def get_source(self, env: Environment, template_name: str) -> TemplateSource:
+    def get_source(self, _: Environment, __: str) -> TemplateSource:
         raise TemplateNotFound(
             f"{self.__class__.__name__} requires a namespace argument"
         )
diff --git a/tests/test_macros.py b/tests/test_macros.py
index 57fac6da..1d1871c7 100644
--- a/tests/test_macros.py
+++ b/tests/test_macros.py
@@ -1,11 +1,10 @@
 """Test cases for `macro` and `call` tags."""
 import asyncio
-from unittest import TestCase
-
-from typing import Mapping
-from typing import NamedTuple
 from typing import Any
 from typing import Dict
+from typing import Mapping
+from typing import NamedTuple
+from unittest import TestCase
 
 from liquid.context import StrictUndefined
 from liquid.environment import Environment
@@ -20,7 +19,7 @@ class Case(NamedTuple):
     description: str
     template: str
     expect: str
-    globals: Mapping[str, Any]
+    globals: Mapping[str, Any]  # noqa: A003
     partials: Dict[str, str]
 
 
diff --git a/tests/test_malformed.py b/tests/test_malformed.py
index 9d5ea2a7..d4e2d8b2 100644
--- a/tests/test_malformed.py
+++ b/tests/test_malformed.py
@@ -1,34 +1,28 @@
 """Malformed template test cases."""
-# pylint: disable=missing-class-docstring missing-function-docstring
-
-from unittest import TestCase
-
-from typing import Iterable
 from typing import Dict
+from typing import Iterable
 from typing import List
-from typing import Union
 from typing import NamedTuple
-from typing import Type
 from typing import Tuple
+from typing import Type
+from typing import Union
+from unittest import TestCase
 
 from liquid import Template
 from liquid.environment import Environment
-from liquid.mode import Mode
-
-from liquid.exceptions import LiquidSyntaxError
+from liquid.exceptions import ContextDepthError
+from liquid.exceptions import DisabledTagError
+from liquid.exceptions import Error
 from liquid.exceptions import FilterArgumentError
+from liquid.exceptions import LiquidSyntaxError
 from liquid.exceptions import LiquidTypeError
-from liquid.exceptions import lookup_warning
-from liquid.exceptions import Error
 from liquid.exceptions import TemplateNotFound
-from liquid.exceptions import DisabledTagError
-from liquid.exceptions import ContextDepthError
-
-from liquid.extra import add_inline_expression_tags
-from liquid.extra import add_extended_inline_expression_tags
+from liquid.exceptions import lookup_warning
 from liquid.extra import IfNotTag
-
+from liquid.extra import add_extended_inline_expression_tags
+from liquid.extra import add_inline_expression_tags
 from liquid.loaders import DictLoader
+from liquid.mode import Mode
 
 
 class Case(NamedTuple):
@@ -127,12 +121,9 @@ def _test_partial(self, test_cases: Iterable[Case], templates: Dict[str, str]):
 
         env = Environment(loader=DictLoader(templates), tolerance=Mode.WARN)
         for case in test_cases:
-            with self.subTest(msg=case.description):
-                with self.assertWarns(case.warnings):
-                    template = env.from_string(
-                        case.template, globals=self.global_context
-                    )
-                    template.render()
+            with self.subTest(msg=case.description), self.assertWarns(case.warnings):
+                template = env.from_string(case.template, globals=self.global_context)
+                template.render()
 
         env = Environment(loader=DictLoader(templates), tolerance=Mode.LAX)
         for case in test_cases:
@@ -143,7 +134,6 @@ def _test_partial(self, test_cases: Iterable[Case], templates: Dict[str, str]):
 
     def test_liquid_syntax(self):
         """Test that we fail early and loud when parsing a malformed template."""
-
         test_cases = [
             Case(
                 description="no expression",
@@ -382,7 +372,6 @@ def test_liquid_syntax_from_template_api(self):
 
     def test_unrecoverable_syntax_errors(self):
         """Test that we fail early and loud when parsing a malformed template."""
-
         test_cases = [
             Case(
                 description="single bracket close",
@@ -402,7 +391,6 @@ def test_unrecoverable_syntax_errors(self):
 
     def test_bad_include(self):
         """Test that we gracefully handle include errors."""
-
         test_cases = [
             Case(
                 description="chained alias identifier",
@@ -473,7 +461,6 @@ def test_bad_include(self):
 
     def test_bad_render(self):
         """Test that we gracefully handle render errors."""
-
         test_cases = [
             Case(
                 description="break from render",
diff --git a/tests/test_parse.py b/tests/test_parse.py
index bb188500..025a2b87 100644
--- a/tests/test_parse.py
+++ b/tests/test_parse.py
@@ -1,33 +1,29 @@
 """Liquid template and expression parsing tests."""
-# pylint: disable=missing-class-docstring missing-function-docstring
-
 import unittest
-
 from typing import Iterable
 from typing import List
 from typing import NamedTuple
 from typing import Union
 
-from liquid.environment import Environment
-from liquid.mode import Mode
-
-from liquid.builtin import literal
-from liquid.builtin import statement
-from liquid.builtin import if_tag
-from liquid.builtin import comment_tag
-from liquid.builtin import unless_tag
-from liquid.builtin import case_tag
-from liquid.builtin import for_tag
-from liquid.builtin import tablerow_tag
+from liquid.builtin import assign_tag
 from liquid.builtin import capture_tag
+from liquid.builtin import case_tag
+from liquid.builtin import comment_tag
 from liquid.builtin import cycle_tag
-from liquid.builtin import assign_tag
-from liquid.builtin import increment_tag
 from liquid.builtin import decrement_tag
 from liquid.builtin import echo_tag
+from liquid.builtin import for_tag
+from liquid.builtin import if_tag
+from liquid.builtin import ifchanged_tag
 from liquid.builtin import include_tag
+from liquid.builtin import increment_tag
+from liquid.builtin import literal
 from liquid.builtin import render_tag
-from liquid.builtin import ifchanged_tag
+from liquid.builtin import statement
+from liquid.builtin import tablerow_tag
+from liquid.builtin import unless_tag
+from liquid.environment import Environment
+from liquid.mode import Mode
 
 
 class Case(NamedTuple):
@@ -43,7 +39,6 @@ def expected(self):
         return self.expect
 
 
-# pylint: disable=too-many-public-methods
 class ParserTestCase(unittest.TestCase):
     def _test(self, test_cases: Iterable[Case], instance, mode: Mode = Mode.STRICT):
         """Helper method for testing lists of `Case`s."""
@@ -85,7 +80,6 @@ def test_output_statement(self):
 
     def test_illegal_tag(self):
         """Test that we can handle illegal tokens in lax mode."""
-
         test_cases = [
             Case(
                 "unknown tag name",
diff --git a/tests/test_parse_arguments.py b/tests/test_parse_arguments.py
index d4860e88..50c20273 100644
--- a/tests/test_parse_arguments.py
+++ b/tests/test_parse_arguments.py
@@ -1,33 +1,29 @@
 """Test cases for parsing list of positional and keyword arguments."""
 import unittest
-
 from typing import List
 from typing import NamedTuple
 from typing import Optional
 from typing import Tuple
 from typing import Union
 
+from liquid.exceptions import Error
+from liquid.exceptions import LiquidSyntaxError
+from liquid.expression import FALSE
+from liquid.expression import NIL
+from liquid.expression import TRUE
 from liquid.expression import Expression
 from liquid.expression import FloatLiteral
-from liquid.expression import IdentifierPathElement
 from liquid.expression import Identifier
+from liquid.expression import IdentifierPathElement
 from liquid.expression import IntegerLiteral
 from liquid.expression import StringLiteral
-from liquid.expression import FALSE
-from liquid.expression import TRUE
-from liquid.expression import NIL
-
 from liquid.expressions import parse_call_arguments
 from liquid.expressions import parse_keyword_arguments
 from liquid.expressions import parse_macro_arguments
-
 from liquid.expressions.arguments.lex import tokenize
 from liquid.expressions.arguments.parse import parse_equals_separated_arguments
 from liquid.expressions.stream import TokenStream
 
-from liquid.exceptions import Error
-from liquid.exceptions import LiquidSyntaxError
-
 Arguments = List[Tuple[Optional[str], Optional[Expression]]]
 
 
diff --git a/tests/test_parse_boolean_expression.py b/tests/test_parse_boolean_expression.py
index bf177f5f..86f3f708 100644
--- a/tests/test_parse_boolean_expression.py
+++ b/tests/test_parse_boolean_expression.py
@@ -1,26 +1,22 @@
-# pylint: disable=missing-class-docstring missing-module-docstring
 import unittest
-
 from typing import NamedTuple
 from typing import Union
 
-from liquid.expression import Expression
+from liquid.exceptions import LiquidSyntaxError
+from liquid.expression import Boolean
 from liquid.expression import BooleanExpression
-from liquid.expression import IdentifierPathElement
+from liquid.expression import Expression
+from liquid.expression import FloatLiteral
 from liquid.expression import Identifier
-from liquid.expression import Boolean
-from liquid.expression import StringLiteral
+from liquid.expression import IdentifierPathElement
+from liquid.expression import InfixExpression
 from liquid.expression import IntegerLiteral
-from liquid.expression import FloatLiteral
 from liquid.expression import RangeLiteral
-from liquid.expression import InfixExpression
-
+from liquid.expression import StringLiteral
 from liquid.expressions import parse_boolean_expression
 from liquid.expressions import parse_boolean_expression_with_parens
 from liquid.expressions.boolean.parse import PrefixExpression
 
-from liquid.exceptions import LiquidSyntaxError
-
 
 class Case(NamedTuple):
     description: str
diff --git a/tests/test_parse_expression.py b/tests/test_parse_expression.py
index d5a9aaa8..78bb08d7 100644
--- a/tests/test_parse_expression.py
+++ b/tests/test_parse_expression.py
@@ -1,41 +1,38 @@
 """Liquid expression parser test cases."""
 
 import unittest
-from typing import NamedTuple, Any
+from typing import Any
+from typing import NamedTuple
 
-from liquid.expression import FilteredExpression
-from liquid.expression import BooleanExpression
 from liquid.expression import AssignmentExpression
-from liquid.expression import LoopExpression
-from liquid.expression import IdentifierPathElement
-from liquid.expression import Identifier
 from liquid.expression import Boolean
+from liquid.expression import BooleanExpression
 from liquid.expression import Filter
-from liquid.expression import StringLiteral
-from liquid.expression import IntegerLiteral
+from liquid.expression import FilteredExpression
 from liquid.expression import FloatLiteral
-from liquid.expression import PrefixExpression
+from liquid.expression import Identifier
+from liquid.expression import IdentifierPathElement
 from liquid.expression import InfixExpression
+from liquid.expression import IntegerLiteral
+from liquid.expression import LoopExpression
+from liquid.expression import PrefixExpression
 from liquid.expression import RangeLiteral
-
-from liquid.token import Token
-from liquid.token import TOKEN_INITIAL
-
-from liquid.stream import TokenStream
-
-from liquid.lex import tokenize_loop_expression
-from liquid.lex import tokenize_filtered_expression
-from liquid.lex import tokenize_boolean_expression
+from liquid.expression import StringLiteral
 from liquid.lex import tokenize_assignment_expression
-
-from liquid.parse import parse_filtered_expression
-from liquid.parse import parse_boolean_expression
+from liquid.lex import tokenize_boolean_expression
+from liquid.lex import tokenize_filtered_expression
+from liquid.lex import tokenize_loop_expression
 from liquid.parse import parse_assignment_expression
+from liquid.parse import parse_boolean_expression
+from liquid.parse import parse_filtered_expression
 from liquid.parse import parse_loop_expression
+from liquid.stream import TokenStream
+from liquid.token import TOKEN_INITIAL
+from liquid.token import Token
 
 
 class Case(NamedTuple):
-    """Subtest helper"""
+    """Subtest helper."""
 
     description: str
     expression: str
@@ -50,7 +47,6 @@ class LiquidFilteredExpressionParserTestCase(unittest.TestCase):
 
     def _test(self, test_cases, lex_func, parse_func):
         """Helper method for testing lists of Cases."""
-
         for case in test_cases:
             with self.subTest(msg=case.description):
                 tokens = TokenStream(lex_func(case.expression))
diff --git a/tests/test_parse_filtered_expression.py b/tests/test_parse_filtered_expression.py
index 1e0e191e..adb43d1d 100644
--- a/tests/test_parse_filtered_expression.py
+++ b/tests/test_parse_filtered_expression.py
@@ -1,27 +1,24 @@
-# pylint: disable=missing-class-docstring missing-module-docstring
 import unittest
 from dataclasses import dataclass
 
+from liquid.exceptions import LiquidSyntaxError
+from liquid.expression import FALSE
+from liquid.expression import TRUE
 from liquid.expression import ConditionalExpression
 from liquid.expression import Expression
+from liquid.expression import Filter
 from liquid.expression import FilteredExpression
-from liquid.expression import InfixExpression
-from liquid.expression import PrefixExpression
-from liquid.expression import IdentifierPathElement
+from liquid.expression import FloatLiteral
 from liquid.expression import Identifier
-from liquid.expression import Filter
-from liquid.expression import StringLiteral
+from liquid.expression import IdentifierPathElement
+from liquid.expression import InfixExpression
 from liquid.expression import IntegerLiteral
-from liquid.expression import FloatLiteral
+from liquid.expression import PrefixExpression
 from liquid.expression import RangeLiteral
-from liquid.expression import TRUE
-from liquid.expression import FALSE
-
-from liquid.expressions import parse_filtered_expression
+from liquid.expression import StringLiteral
 from liquid.expressions import parse_conditional_expression
 from liquid.expressions import parse_conditional_expression_with_parens
-
-from liquid.exceptions import LiquidSyntaxError
+from liquid.expressions import parse_filtered_expression
 
 
 @dataclass
diff --git a/tests/test_parse_literal_or_identifier.py b/tests/test_parse_literal_or_identifier.py
index 3bd1b993..5c1f1a2c 100644
--- a/tests/test_parse_literal_or_identifier.py
+++ b/tests/test_parse_literal_or_identifier.py
@@ -1,34 +1,32 @@
 """Test cases for parsing common Liquid literals and identifiers."""
 import unittest
 
-from liquid.expressions.common import parse_common_expression
-from liquid.expressions.common import tokenize_common_expression
-from liquid.expressions.stream import TokenStream
-
+from liquid.expression import BLANK
+from liquid.expression import EMPTY
+from liquid.expression import FALSE
+from liquid.expression import NIL
+from liquid.expression import TRUE
 from liquid.expression import FloatLiteral
 from liquid.expression import Identifier
 from liquid.expression import IdentifierPathElement
 from liquid.expression import IntegerLiteral
 from liquid.expression import RangeLiteral
 from liquid.expression import StringLiteral
-from liquid.expression import BLANK
-from liquid.expression import EMPTY
-from liquid.expression import TRUE
-from liquid.expression import FALSE
-from liquid.expression import NIL
-
+from liquid.expressions.common import parse_common_expression
+from liquid.expressions.common import tokenize_common_expression
+from liquid.expressions.stream import TokenStream
 from liquid.token import TOKEN_BLANK
 from liquid.token import TOKEN_EMPTY
 from liquid.token import TOKEN_FALSE
-from liquid.token import TOKEN_TRUE
 from liquid.token import TOKEN_FLOAT
+from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_IDENTINDEX
 from liquid.token import TOKEN_INTEGER
 from liquid.token import TOKEN_NIL
+from liquid.token import TOKEN_RBRACKET
 from liquid.token import TOKEN_RPAREN
 from liquid.token import TOKEN_STRING
-from liquid.token import TOKEN_IDENTINDEX
-from liquid.token import TOKEN_IDENTIFIER
-from liquid.token import TOKEN_RBRACKET
+from liquid.token import TOKEN_TRUE
 
 
 class ParseLiteralOrIdentifierTestCase(unittest.TestCase):
diff --git a/tests/test_parse_loop_expression.py b/tests/test_parse_loop_expression.py
index ae18fdda..abcc8ba3 100644
--- a/tests/test_parse_loop_expression.py
+++ b/tests/test_parse_loop_expression.py
@@ -1,16 +1,13 @@
-# pylint: disable=missing-class-docstring missing-module-docstring
 import unittest
-
 from dataclasses import dataclass
 from typing import Union
 
 from liquid.expression import Expression
-from liquid.expression import IdentifierPathElement
 from liquid.expression import Identifier
+from liquid.expression import IdentifierPathElement
 from liquid.expression import IntegerLiteral
-from liquid.expression import RangeLiteral
 from liquid.expression import LoopExpression
-
+from liquid.expression import RangeLiteral
 from liquid.expressions import parse_loop_expression
 
 
diff --git a/tests/test_render.py b/tests/test_render.py
index b998976b..a9a548de 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -1,21 +1,18 @@
 """Liquid render tests."""
-# pylint: disable=missing-function-docstring,too-many-public-methods,missing-class-docstring
 import asyncio
 import unittest
-
 from typing import List
 
+from liquid import golden
 from liquid.environment import Environment
+from liquid.exceptions import Error
 from liquid.future import Environment as FutureEnvironment
+from liquid.golden.case import Case
+from liquid.loaders import DictLoader
+from liquid.mode import Mode
 from liquid.template import AwareBoundTemplate
 from liquid.template import BoundTemplate
 from liquid.template import FutureAwareBoundTemplate
-from liquid.mode import Mode
-from liquid.loaders import DictLoader
-
-from liquid import golden
-from liquid.exceptions import Error
-from liquid.golden.case import Case
 
 
 class RenderTestCase(unittest.TestCase):
diff --git a/tests/test_render_extra.py b/tests/test_render_extra.py
index 0303fa21..4c55c1eb 100644
--- a/tests/test_render_extra.py
+++ b/tests/test_render_extra.py
@@ -1,19 +1,21 @@
 """Test cases for rendering non-standard "extra" tags."""
 import asyncio
 import unittest
+from typing import TYPE_CHECKING
 from typing import List
 
 from liquid import golden
 from liquid.environment import Environment
 from liquid.exceptions import Error
-from liquid.golden.case import Case
-from liquid.loaders import DictLoader
-from liquid.template import BoundTemplate
-
 from liquid.extra import IfNotTag
 from liquid.extra import WithTag
-from liquid.extra import add_inline_expression_tags
 from liquid.extra import add_extended_inline_expression_tags
+from liquid.extra import add_inline_expression_tags
+from liquid.golden.case import Case
+from liquid.loaders import DictLoader
+
+if TYPE_CHECKING:
+    from liquid.template import BoundTemplate
 
 
 class BaseRenderTestCase(unittest.TestCase):
@@ -40,7 +42,7 @@ def _test(self, test_cases: List[Case]) -> None:
                     result = template.render()
                     self.assertEqual(result, case.expect)
 
-        async def coro(template: BoundTemplate):
+        async def coro(template: "BoundTemplate"):
             return await template.render_async()
 
         for case in test_cases:
diff --git a/tests/test_resource_limits.py b/tests/test_resource_limits.py
index dcff1c54..67d54669 100644
--- a/tests/test_resource_limits.py
+++ b/tests/test_resource_limits.py
@@ -1,14 +1,12 @@
 """Resource limit test cases."""
-# pylint: disable=missing-class-docstring
 import platform
 import unittest
 
 from liquid import DictLoader
 from liquid import Environment
-
 from liquid.exceptions import ContextDepthError
-from liquid.exceptions import LoopIterationLimitError
 from liquid.exceptions import LocalNamespaceLimitError
+from liquid.exceptions import LoopIterationLimitError
 from liquid.exceptions import OutputStreamLimitError
 
 
@@ -161,10 +159,8 @@ class MockEnv(Environment):
         env = MockEnv(
             loader=DictLoader(
                 {
-                    "foo": (
-                        "{% for i in (1..50) %}" "{% render 'bar' %}" "{% endfor %}"
-                    ),
-                    "bar": ("{% for j in (1..50) %}" "{{ j }}" "{% endfor %}"),
+                    "foo": ("{% for i in (1..50) %}{% render 'bar' %}{% endfor %}"),
+                    "bar": ("{% for j in (1..50) %}{{ j }}{% endfor %}"),
                 }
             )
         )
diff --git a/tests/test_shorthand_comments.py b/tests/test_shorthand_comments.py
index 100b7c8b..f49af433 100644
--- a/tests/test_shorthand_comments.py
+++ b/tests/test_shorthand_comments.py
@@ -1,16 +1,13 @@
 """Test cases for shorthand comment syntax."""
-# pylint: disable=missing-class-docstring
-
 from dataclasses import dataclass
-from unittest import TestCase
-
 from typing import List
 from typing import Type
 from typing import Union
+from unittest import TestCase
 
 from liquid import Environment
-from liquid import Template
 from liquid import Mode
+from liquid import Template
 
 
 @dataclass
diff --git a/tests/test_template_api.py b/tests/test_template_api.py
index 09cc52b7..fdc6d951 100644
--- a/tests/test_template_api.py
+++ b/tests/test_template_api.py
@@ -2,9 +2,9 @@
 
 import unittest
 
-from liquid import Template
 from liquid import Environment
 from liquid import Mode
+from liquid import Template
 
 
 class TemplateAPITestCase(unittest.TestCase):
diff --git a/tests/test_template_cache.py b/tests/test_template_cache.py
index cdf740ad..6d2280cd 100644
--- a/tests/test_template_cache.py
+++ b/tests/test_template_cache.py
@@ -1,5 +1,4 @@
 """Template cache test cases."""
-# pylint: disable=missing-class-docstring
 import unittest
 
 from liquid.utils import LRUCache
diff --git a/tests/test_template_inheritance.py b/tests/test_template_inheritance.py
index 4f1e1ab4..6b388a13 100644
--- a/tests/test_template_inheritance.py
+++ b/tests/test_template_inheritance.py
@@ -1,19 +1,18 @@
 """Test cases for `extends` and `block` tags."""
 import asyncio
-from unittest import TestCase
-
-from typing import Mapping
-from typing import NamedTuple
 from typing import Any
 from typing import Dict
+from typing import Mapping
+from typing import NamedTuple
+from unittest import TestCase
 
 from liquid import BoundTemplate
 from liquid import Environment
 from liquid import StrictUndefined
 from liquid.exceptions import LiquidSyntaxError
 from liquid.exceptions import RequiredBlockError
-from liquid.exceptions import UndefinedError
 from liquid.exceptions import TemplateInheritanceError
+from liquid.exceptions import UndefinedError
 from liquid.extra import add_inheritance_tags
 from liquid.loaders import DictLoader
 
@@ -24,7 +23,7 @@ class Case(NamedTuple):
     description: str
     template: str
     expect: str
-    globals: Mapping[str, Any]
+    globals: Mapping[str, Any]  # noqa: A003
     partials: Dict[str, str]
 
 
@@ -350,9 +349,8 @@ async def coro(template: BoundTemplate) -> str:
         with self.assertRaises(RequiredBlockError):
             template.render()
 
-        with self.subTest(asynchronous=True):
-            with self.assertRaises(RequiredBlockError):
-                asyncio.run(coro(template))
+        with self.subTest(asynchronous=True), self.assertRaises(RequiredBlockError):
+            asyncio.run(coro(template))
 
     def test_missing_required_block_long_stack(self) -> None:
         """Test that we raise an exception if a required block is missing in a long
@@ -373,9 +371,8 @@ async def coro(template: BoundTemplate) -> str:
         with self.assertRaises(RequiredBlockError):
             template.render()
 
-        with self.subTest(asynchronous=True):
-            with self.assertRaises(RequiredBlockError):
-                asyncio.run(coro(template))
+        with self.subTest(asynchronous=True), self.assertRaises(RequiredBlockError):
+            asyncio.run(coro(template))
 
     def test_immediate_override_required_block(self) -> None:
         """Test that we can override a required block in the immediate child."""
@@ -485,9 +482,8 @@ async def coro(template: BoundTemplate) -> str:
         with self.assertRaises(RequiredBlockError):
             template.render()
 
-        with self.subTest(asynchronous=True):
-            with self.assertRaises(RequiredBlockError):
-                asyncio.run(coro(template))
+        with self.subTest(asynchronous=True), self.assertRaises(RequiredBlockError):
+            asyncio.run(coro(template))
 
     def test_override_required_block_directly(self) -> None:
         """Test that we raise an exception if rendering a required block directly."""
@@ -503,9 +499,8 @@ async def coro(template: BoundTemplate) -> str:
         with self.assertRaises(RequiredBlockError):
             template.render()
 
-        with self.subTest(asynchronous=True):
-            with self.assertRaises(RequiredBlockError):
-                asyncio.run(coro(template))
+        with self.subTest(asynchronous=True), self.assertRaises(RequiredBlockError):
+            asyncio.run(coro(template))
 
     def test_too_many_extends(self) -> None:
         """Test that we raise an exception if more than one `extends` tag exists."""
@@ -542,12 +537,11 @@ async def coro(template: BoundTemplate) -> str:
         with self.assertRaises(LiquidSyntaxError):
             template.render()
 
-        with self.subTest(asynchronous=True):
-            with self.assertRaises(LiquidSyntaxError):
-                asyncio.run(coro(template))
+        with self.subTest(asynchronous=True), self.assertRaises(LiquidSyntaxError):
+            asyncio.run(coro(template))
 
     def test_block_drop_properties(self) -> None:
-        """Test that we handle undefined block drop properties"""
+        """Test that we handle undefined block drop properties."""
         template = (
             "{% extends 'foo' %}"
             "{% block bar %}{{ block.nosuchthing }} and sue{% endblock %}"
@@ -564,12 +558,11 @@ async def coro(template: BoundTemplate) -> str:
         with self.assertRaises(UndefinedError):
             template.render()
 
-        with self.subTest(asynchronous=True):
-            with self.assertRaises(UndefinedError):
-                asyncio.run(coro(template))
+        with self.subTest(asynchronous=True), self.assertRaises(UndefinedError):
+            asyncio.run(coro(template))
 
     def test_block_no_super_block(self) -> None:
-        """Test that we handle undefined block.super"""
+        """Test that we handle undefined block.super."""
         template = "hello, {% block bar %}{{ block.super }}{{ you }}{% endblock %}"
 
         async def coro(template: BoundTemplate) -> str:
@@ -582,9 +575,8 @@ async def coro(template: BoundTemplate) -> str:
         with self.assertRaises(UndefinedError):
             template.render()
 
-        with self.subTest(asynchronous=True):
-            with self.assertRaises(UndefinedError):
-                asyncio.run(coro(template))
+        with self.subTest(asynchronous=True), self.assertRaises(UndefinedError):
+            asyncio.run(coro(template))
 
     def test_duplicate_block_names(self) -> None:
         """Test that we raise an exception when a template has duplicate block names."""
diff --git a/tests/test_token_stream.py b/tests/test_token_stream.py
index 92aeaab6..ea79f9c4 100644
--- a/tests/test_token_stream.py
+++ b/tests/test_token_stream.py
@@ -1,16 +1,15 @@
 """Token stream test cases."""
 from unittest import TestCase
 
-from liquid.token import Token
-from liquid.token import TOKEN_STRING
-from liquid.token import TOKEN_PIPE
-from liquid.token import TOKEN_IDENTIFIER
+from liquid.stream import TokenStream
 from liquid.token import TOKEN_COLON
 from liquid.token import TOKEN_COMMA
-from liquid.token import TOKEN_INTEGER
 from liquid.token import TOKEN_EOF
-
-from liquid.stream import TokenStream
+from liquid.token import TOKEN_IDENTIFIER
+from liquid.token import TOKEN_INTEGER
+from liquid.token import TOKEN_PIPE
+from liquid.token import TOKEN_STRING
+from liquid.token import Token
 
 
 class TokenStreamTestCase(TestCase):
diff --git a/tests/test_undefined.py b/tests/test_undefined.py
index 78bda0f4..ffa70747 100644
--- a/tests/test_undefined.py
+++ b/tests/test_undefined.py
@@ -1,22 +1,21 @@
 """Test built-in "undefined" types."""
 import asyncio
-
 from dataclasses import dataclass
 from dataclasses import field
-
-from unittest import TestCase
+from typing import TYPE_CHECKING
 from typing import Dict
+from unittest import TestCase
 
-from liquid import Environment
-from liquid import Undefined
 from liquid import DebugUndefined
-from liquid import StrictUndefined
+from liquid import Environment
 from liquid import StrictDefaultUndefined
-
-from liquid.template import BoundTemplate
-
-from liquid.exceptions import UndefinedError
+from liquid import StrictUndefined
+from liquid import Undefined
 from liquid.exceptions import NoSuchFilterFunc
+from liquid.exceptions import UndefinedError
+
+if TYPE_CHECKING:
+    from liquid.template import BoundTemplate
 
 
 @dataclass
@@ -272,7 +271,7 @@ def test_isinstance_strict_default_filter(self):
         __class__."""
         undef = StrictDefaultUndefined("nosuchthing")
         with self.assertRaises(UndefinedError):
-            undef.__class__  # pylint: disable=pointless-statement
+            undef.__class__  # noqa: B018
 
     def test_strict_default_undefined(self):
         """Test that the strict default undefined type raises an exception for
@@ -427,7 +426,7 @@ def test_strict_filters(self):
                 self.assertEqual(case.expect, str(raised.exception))
 
                 # And render async
-                async def coro(template: BoundTemplate):
+                async def coro(template: "BoundTemplate"):
                     return await template.render_async()
 
                 with self.assertRaises(NoSuchFilterFunc) as raised: