aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2022-09-10 15:55:47 +0200
committerDaniel Schadt <kingdread@gmx.de>2022-09-10 15:55:47 +0200
commitd4d5964765163afe75fc272a87a7b1ec2b582263 (patch)
treea4305ba433dceb696b12f5a1927d149caa07d919
parentb7dee45781af030debdd109c2ee8ddc4acca0616 (diff)
downloadfietsboek-d4d5964765163afe75fc272a87a7b1ec2b582263.tar.gz
fietsboek-d4d5964765163afe75fc272a87a7b1ec2b582263.tar.bz2
fietsboek-d4d5964765163afe75fc272a87a7b1ec2b582263.zip
first implementation of update logic
-rw-r--r--fietsboek/updater/__init__.py343
-rw-r--r--pylint.toml523
-rw-r--r--tox.ini2
3 files changed, 867 insertions, 1 deletions
diff --git a/fietsboek/updater/__init__.py b/fietsboek/updater/__init__.py
new file mode 100644
index 0000000..bc1bc96
--- /dev/null
+++ b/fietsboek/updater/__init__.py
@@ -0,0 +1,343 @@
+"""Updating (data migration) logic for fietsboek."""
+import logging
+import datetime
+import random
+import string
+import importlib.util
+from pathlib import Path
+
+# Compat for Python < 3.9
+import importlib_resources
+import pyramid.paster
+import alembic.runtime
+import alembic.config
+import alembic.command
+import sqlalchemy
+import jinja2
+
+
+LOGGER = logging.getLogger(__name__)
+
+TEMPLATE = """\
+\"\"\"Revision upgrade script {{ update_id }}
+
+Date created: {{ date }}
+\"\"\"
+update_id = {{ "{!r}".format(update_id) }}
+previous = [
+{%- for prev in previous %}
+ {{ "{!r}".format(prev) }},
+{% endfor -%}
+]
+alembic_revision = {{ "{!r}".format(alembic_revision) }}
+
+
+class Up:
+ def pre_alembic(self, config):
+ pass
+
+ def post_alembic(self, config):
+ pass
+
+
+class Down:
+ def pre_alembic(self, config):
+ pass
+
+ def post_alembic(self, config):
+ pass
+"""
+
+
+class Updater:
+ """A class that implements the updating logic.
+
+ This class is responsible for holding all of the update scripts and running
+ them in the right order.
+ """
+
+ def __init__(self, config_path):
+ self.config_path = config_path
+ self.settings = pyramid.paster.get_appsettings(config_path)
+ self.alembic_config = alembic.config.Config(config_path)
+ self.scripts = {}
+ self.forward_dependencies = {}
+ self.backward_dependencies = {}
+
+ @property
+ def version_file(self):
+ """Returns the path to the version file.
+
+ :return: The path to the data's version file.
+ :rytpe: pathlib.Path
+ """
+ data_dir = Path(self.settings["fietsboek.data_dir"])
+ return data_dir / "VERSION"
+
+ def load(self):
+ """Load all update scripts into memory."""
+ scripts = _load_update_scripts()
+ for script in scripts:
+ self.scripts[script.id] = script
+ self.forward_dependencies = {
+ script.id: script.previous for script in self.scripts.values()
+ }
+ # Ensure that each script has an entry
+ self.backward_dependencies = {
+ script.id: [] for script in self.scripts.values()
+ }
+ for script in self.scripts.values():
+ for prev_id in script.previous:
+ self.backward_dependencies[prev_id].append(script.id)
+
+ def current_versions(self):
+ """Reads the current version of the data.
+
+ :rtype: list[str]
+ :return: The versions, or an empty list if no versions are found.
+ """
+ try:
+ versions = self.version_file.read_text().split("\n")
+ return [version.strip() for version in versions if version.strip()]
+ except FileNotFoundError:
+ return []
+
+ def _transitive_versions(self):
+ versions = set()
+ queue = self.current_versions()
+ while queue:
+ current = queue.pop()
+ versions.add(current)
+ if current in self.scripts:
+ queue.extend(self.scripts[current].previous)
+ return versions
+
+ def _reverse_versions(self):
+ all_versions = set(script.id for script in self.scripts.values())
+ return (all_versions - self._transitive_versions()) | set(self.current_versions())
+
+ def stamp(self, versions):
+ """Stampts the given version into the version file.
+
+ This does not run any updates, it simply updates the version information.
+
+ :param version: The versions to stamp.
+ :type version: list[str]
+ """
+ self.version_file.write_text("\n".join(versions))
+
+ def _pick_updates(self, wanted, applied, dependencies):
+ to_apply = set()
+ queue = [wanted]
+ while queue:
+ current = queue.pop(0)
+ if current in applied or current in to_apply:
+ continue
+ to_apply.add(current)
+ queue.extend(dependencies[current])
+ return to_apply
+
+ def _make_schedule(self, wanted, dependencies):
+ wanted = set(wanted)
+ queue = []
+ while wanted:
+ next_updates = {
+ update
+ for update in wanted
+ if all(previous not in wanted for previous in dependencies[update])
+ }
+ queue.extend(next_updates)
+ wanted -= next_updates
+ return queue
+
+ def _stamp_versions(self, old, new):
+ versions = self.current_versions()
+ versions = [version for version in versions if version not in old]
+ versions.extend(new)
+ self.stamp(versions)
+
+ def upgrade(self, target):
+ """Run the tasks to upgrade to the given target.
+
+ This ensures that all previous migrations are also run.
+
+ :param target: The target revision.
+ :type target: str
+ """
+ # First, we figure out which tasks we have already applied and which
+ # still need applying. This is pretty much a BFS over the current
+ # version and its dependencies.
+ applied_versions = self._transitive_versions()
+ to_apply = self._pick_updates(target, applied_versions, self.forward_dependencies)
+ # Second, we need to ensure that the tasks are applied in the right
+ # order (topological sort)
+ application_queue = self._make_schedule(to_apply, self.forward_dependencies)
+ # Finally, we can run the updates
+ LOGGER.debug("Planned update: %s", application_queue)
+ for update in application_queue:
+ script = self.scripts[update]
+ script.upgrade(self.settings, self.alembic_config)
+ self._stamp_versions(script.previous, [script.id])
+
+ def downgrade(self, target):
+ """Run the tasks to downgrade to the given target.
+
+ This ensures that all succeeding down-migrations are also run.
+
+ :param target: The target revision.
+ :type target: str
+ """
+ # This is basically the same as upgrade() but with the reverse
+ # dependencies instead.
+ applied_versions = self._reverse_versions()
+ to_apply = self._pick_updates(target, applied_versions, self.backward_dependencies)
+ application_queue = self._make_schedule(to_apply, self.backward_dependencies)
+ LOGGER.debug("Planned downgrade: %s", application_queue)
+ for downgrade in application_queue:
+ script = self.scripts[downgrade]
+ script.downgrade(self.settings, self.alembic_config)
+ self._stamp_versions(self.backward_dependencies[script.id], [script.id])
+
+ def new_revision(self, revision_id=None):
+ """Creates a new revision with the current versions as dependencies and
+ the current alembic version.
+
+ :param revision_id: The revision ID to use. By default, a random string
+ will be generated.
+ :type revision_id: str
+ :return: The filename of the revision file in the ``updater/``
+ directory.
+ :rtype: str
+ """
+ if not revision_id:
+ revision_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=16))
+
+ current_versions = self.current_versions()
+
+ engine = sqlalchemy.create_engine(self.settings["sqlalchemy.url"])
+ with engine.connect() as conn:
+ context = alembic.runtime.migration.MigrationContext.configure(conn)
+ current_alembic = context.get_current_heads()
+ LOGGER.debug("Found alembic versions: %s", current_alembic)
+ assert len(current_alembic) == 1
+ current_alembic = current_alembic[0]
+
+ loader = jinja2.DictLoader({"revision.py": TEMPLATE})
+ env = jinja2.Environment(loader=loader, autoescape=False)
+ template = env.get_template("revision.py")
+ revision = template.render(
+ update_id=revision_id,
+ previous=current_versions,
+ alembic_revision=current_alembic,
+ date=datetime.datetime.now(),
+ )
+
+ filename = f"upd_{revision_id}.py"
+ filepath = Path(__file__).parent / filename
+ LOGGER.info("Writing new revision (%s) to %r", revision_id, filepath)
+ with open(filepath, "x", encoding="utf-8") as fobj:
+ fobj.write(revision)
+ return filename
+
+ def heads(self):
+ """Returns all "heads", that are the latest revisions.
+
+ :return: The heads.
+ :rtype: list[str]
+ """
+ return [rev_id for (rev_id, deps) in self.backward_dependencies.items() if not deps]
+
+
+class UpdateScript:
+ """Represents an update script."""
+
+ def __init__(self, source, name):
+ self.name = name
+ spec = importlib.util.spec_from_loader(f"{__name__}.{name}", None)
+ self.module = importlib.util.module_from_spec(spec)
+ exec(source, self.module.__dict__) # pylint: disable=exec-used
+
+ def __repr__(self):
+ return f"<{__name__}.{self.__class__.__name__} name={self.name!r} id={self.id!r}>"
+
+ @property
+ def id(self):
+ """Returns the ID of the update.
+
+ :rtype: str
+ :return: The id of the update
+ """
+ return self.module.update_id
+
+ @property
+ def previous(self):
+ """Returns all dependencies of the update.
+
+ :rtype: list[str]
+ :return: The IDs of all dependencies of the update.
+ """
+ return getattr(self.module, "previous", [])
+
+ @property
+ def alembic_version(self):
+ """Returns the alembic revisions of the update.
+
+ :rtype: list[str]
+ :return: The needed alembic revisions.
+ """
+ return self.module.alembic_revision
+
+ def upgrade(self, config, alembic_config):
+ """Runs the upgrade migrations of this update script.
+
+ This first runs the pre_alembic task, then the alembic migration, and
+ finally the post_alembic task.
+
+ Note that this does not ensure that all previous scripts have also been
+ executed.
+
+ :param config: The app configuration.
+ :type config: dict
+ :param alembic_config: The alembic config to use.
+ :type alembic_config: alembic.config.Config
+ """
+ LOGGER.info("[up] Running pre-alembic task for %s", self.id)
+ self.module.Up().pre_alembic(config)
+ LOGGER.info("[up] Running alembic upgrade for %s to %s", self.id, self.alembic_version)
+ alembic.command.upgrade(alembic_config, self.alembic_version)
+ LOGGER.info("[up] Running post-alembic task for %s", self.id)
+ self.module.Up().post_alembic(config)
+
+ def downgrade(self, config, alembic_config):
+ """Runs the downgrade migrations of this update script.
+
+ See also :meth:`upgrade`.
+
+ :param config: The app configuration.
+ :type config: dict
+ :param alembic_config: The alembic config to use.
+ :type alembic_config: alembic.config.Config
+ """
+ LOGGER.info("[down] Running pre-alembic task for %s", self.id)
+ self.module.Down().pre_alembic(config)
+ LOGGER.info("[down] Running alembic downgrade for %s to %s", self.id, self.alembic_version)
+ alembic.command.downgrade(alembic_config, self.alembic_version)
+ LOGGER.info("[down] Running post-alembic task for %s", self.id)
+ self.module.Down().post_alembic(config)
+
+
+def _filename_to_modname(name):
+ if name.endswith(".py"):
+ name = name[:-3]
+ name = name.replace(".", "_")
+ return name
+
+
+def _load_update_scripts():
+ """Loads all available import scripts."""
+ files = importlib_resources.files(__name__)
+ return [
+ UpdateScript(file.read_text(), _filename_to_modname(file.name))
+ for file in files.iterdir()
+ if file.name.startswith("upd_")
+ ]
diff --git a/pylint.toml b/pylint.toml
new file mode 100644
index 0000000..7495cf7
--- /dev/null
+++ b/pylint.toml
@@ -0,0 +1,523 @@
+[tool.pylint.main]
+# 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 =
+
+# Always return a 0 (non-error) status code, even if lint errors are found. This
+# is primarily useful in continuous integration scripts.
+# exit-zero =
+
+# 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
+
+# Interpret the stdin as a python script, whose filename needs to be passed as
+# the module_or_package argument.
+# from-stdin =
+
+# 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 = ["fietsboek/updater/upd_.*.py"]
+
+# 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 = ["^\\.#"]
+
+# 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 =
+
+# 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, and will cap the count on Windows to
+# avoid hangs.
+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 = true
+
+# 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 =
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode = true
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+# unsafe-load-any-extension =
+
+[tool.pylint.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", "_", "id"]
+
+# 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 =
+
+# 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 =
+
+[tool.pylint.classes]
+# Warn about protected attribute access inside special methods
+# check-protected-access-in-special-methods =
+
+# 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"]
+
+[tool.pylint.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 = 5
+
+# 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
+
+[tool.pylint.exceptions]
+# Exceptions that will emit a warning when caught.
+overgeneral-exceptions = ["BaseException", "Exception"]
+
+[tool.pylint.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 =
+
+# Allow the body of an if to be on the same line as the test if there is no else.
+# single-line-if-stmt =
+
+[tool.pylint.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 =
+
+# 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 =
+
+[tool.pylint.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"]
+
+[tool.pylint."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 = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"]
+
+# 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"]
+
+# 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"]
+
+[tool.pylint.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 =
+
+[tool.pylint.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"]
+
+[tool.pylint.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 =
+
+# Tells whether to display a full report or only the messages.
+# reports =
+
+# Activate the evaluation score.
+score = true
+
+[tool.pylint.similarities]
+# Comments are removed from the similarity computation
+ignore-comments = true
+
+# Docstrings are removed from the similarity computation
+ignore-docstrings = true
+
+# Imports are removed from the similarity computation
+ignore-imports = true
+
+# Signatures are removed from the similarity computation
+ignore-signatures = true
+
+# Minimum lines number of a similarity.
+min-similarity-lines = 4
+
+[tool.pylint.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 at 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 =
+
+[tool.pylint.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 =
+
+# 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 =
+
+[tool.pylint.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.
+# Tells whether to warn about missing members when the owner of the attribute is
+# inferred to be None.
+ignore-none = true
+
+# 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 = true
+
+# List of symbolic message names to ignore for Mixin members.
+ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"]
+
+# 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", "argparse.Namespace"]
+
+# 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 = true
+
+# 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.
+mixin-class-rgx = ".*[Mm]ixin"
+
+# List of decorators that change the signature of a decorated function.
+# signature-mutators =
+
+[tool.pylint.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 = true
+
+# 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 =
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules = ["six.moves", "past.builtins", "future.builtins", "builtins", "io"]
+
+
diff --git a/tox.ini b/tox.ini
index da41521..91cda20 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,7 +21,7 @@ commands =
deps = pylint
usedevelop = true
commands =
- pylint fietsboek
+ pylint --rcfile=pylint.toml fietsboek
[testenv:pylint-tests]
deps = pylint