Plugins

Plugins are the providers of the basic units for the application of rules, defining actions which can be either verbs or conditionals.

  • conditionals filter the execution of verbs in a rule. If all the conditionals of a rule returns true, the verbs are executed. Conditionals are identified because they start with the prefix if.

  • verbs execute actions against the files defined in the special files property of each rule. They act like asserters.

inclusion

Files content inclusion management.

includeLines

Check that the files include expected lines.

Accept an array of strings as the lines to exclude or an array of arrays with the content to exclude and the fixer query as two strings.

Examples

Appends lines not already present in the file to the end.

{
  rules: [
    files: [".gitignore"],
    includeLines: ["venv*/", "/dist/"]
  ]
}

New in version 0.1.0.

Changed in version 0.7.0: Accept arrays of [line, fixer_query] as items of the array to edit manually the files using JMESPath queries.

ifIncludeLines

Conditional to exclude rule only if some files include a set of lines.

If one file don’t include all lines passed as parameter, the rule will be ignored.

Accept an object mapping files to lines that must be included in order to execute the rule.

Example

If the license defined in the LICENSE file is BSD-3, project.license must correspont:

{
  rules: [
    files: ["pyproject.toml"],
    ifIncludeLines: {
      LICENSE: ["BSD 3-Clause License"],
    },
    JMESPathsMatch: [
      ["project.license", "BSD-3-License"],
    ]
  ]
}

New in version 0.1.0.

includeContent

Check that the files include certain contents.

Accept an array of strings as the contents to include or an array of arrays with the content to include and the fixer query as two strings.

The specified partial contents can match multiple lines and line ending characters. It just raises errors if the passed contents are substrings of each file content.

Example

{
  rules: [
    files: ["setup.py"],
    includeContent: [
      'Installation using setup.py is no longer supported.',
    ]
  ]
}

New in version 0.8.0.

excludeLines

Check that the files do not include certain lines.

Accept an array of strings as the lines to exclude or an array of arrays with the line to exclude and the fixer query as two strings.

Example

{
  rules: [
    hint: 'Keep the .vscode folder',
    files: ['.gitignore'],
    excludeLines: [
      '.vscode'
      '.vscode/',
      '/.vscode',
      '/.vscode/',
    ]
  ]
}

New in version 0.8.0.

excludeContent

Check that the files do not include certain content.

Accept an array of strings as the contents to exclude or an array of arrays with the content to exclude and the fixer query as two strings.

The specified partial contents can match multiple lines and line ending characters. It just raises errors if the passed contents are substrings of each file content.

Example

Don’t allow code blocks in RST documentation files:

  • Bash is not a POSIX compliant shell, use Shell lexer.

  • Pygments’ JSON5 lexer is not implemented yet, use Javascript lexer.

{
  rules: [
    {
      files: ["file.rst"],
      excludeContent: [
        [
          ".. code-block::  ",
          "map(&replace(@, 'code-block::  ', 'code-block:: '), @)",
        ],
        [
          ".. code-block:: bash",
          "map(&replace(@, 'code-block:: bash', 'code-block:: sh'), @)",
        ],
        [
          ".. code-block:: json5",
          "map(&replace(@, 'code-block:: json5', 'code-block:: js'), @)",
        ],
      ],
    },
  ],
}

New in version 0.3.0.

Changed in version 0.7.0: Accept an array ['content-to-exclude', 'fixer-query'] for each item in the array to perform editions in the file if the content is found.

existence

Check existence of files.

ifFilesExist

Check if a set of files and/or directories exists.

Accept an array of paths. If a path ends with / character it is considered a directory.

Examples

If the directory src/ exists, a pyproject.toml file must exist also:

{
  rules: [
    files: ["pyproject.toml"],
    ifFilesExist: ["src/"],
  ]
}

New in version 0.4.0.

jmespath

JMES paths manipulation against files.

The actions of this plugin operates against object-serialized versions of files, so only files that can be serialized can be targetted (see Objects serialization).

You can use in expressions all JMESPath builtin functions plus a set of convenient functions defined by the plugin internally:

Standard JMESPath functions

The next functions extends those functions that the official JMESPath library has accepted and are compatible, but they offer some extra features:

starts_with(search: str, prefix: str[, start: int=0[, end: int=-1]]) bool

Return true if string starts with the prefix, otherwise return false. The argument prefix can also be an array of prefixes to look for. With optional start, test string beginning at that position. With optional end, stop comparing string at that position.

In the official implementation of JMESPath, the start and end parameters are not included and prefix can only be a string.

New in version 0.1.0.

Changed in version 0.7.6:

  • Added start and end parameters.

  • Added support for prefix to be an array of prefixes.

ends_with(search: str, suffix: str[, start: int=0[, end: int=-1]]) bool

Return true if the string ends with the specified suffix, otherwise return false. The argument suffix can also be a tuple of suffixes to look for. With optional start, test beginning at that position. With optional end, stop comparing at that position.

In the official implementation of JMESPath, the start and end parameters are not included and suffix can only be a string.

New in version 0.1.0.

Changed in version 0.7.6:

  • Added start and end parameters.

  • Added support for suffix to be an array of suffixes.

Regex functions

All functions whose name start with regex_ are regex functions, which always takes the regex to apply as the first parameter following the Python’s regex standard library syntax.

regex_match(pattern: str, string: str[, flags: int=0]) bool

Match a regular expression against a string using the Python’s built-in re.match() function.

New in version 0.1.0.

Changed in version 0.5.0: Allow to pass flags optional argument as an integer.

regex_matchall(pattern: str, strings: list[str]) bool

Match a regular expression against a set of strings defined in an array using the Python’s built-in re.match() function.

New in version 0.1.0.

Deprecated since version 0.4.0.

Search using a regular expression against a string using the Python’s built-in re.search() function. Returns all found groups in an array or an array with the full match as the unique item if no groups are defined. If no results are found, returns an empty array.

New in version 0.1.0.

Changed in version 0.5.0: Allow to pass flags optional argument as an integer.

regex_sub(pattern: str, repl: str, string: str[, count: int=0[, flags: int=0]]) str

Replace using a regular expression against a string using the Python’s built-in re.sub() function.

New in version 0.5.0.

regex_escape(pattern: str) str

Escape a regular expression pattern using the Python’s built-in re.escape() function.

New in version 0.7.5.

Utility functions

os() str

Return the result of the Python’s variable sys.platform.

New in version 0.7.0.

getenv(envvar: str) str

Return the value of an environment variable.

New in version 0.7.2.

setenv(envvar: str, value: str | None) dict

Set the value of an environment variable. If you set the value to null, the environment variable will be removed.

Return the updated environment object.

New in version 0.7.2.

rootdir_name() str

Returns the name if the root directory of the project, that will be the current working directory or other that could be either passed in project-config --rootdir CLI option or defined in cli.rootdir configuration option.

New in version 0.6.0.

op(source: any, operation: str, target: any[, operation: str, target: any]...) any

Apply the operator operation between the values source and target using the operators defined in the Python standard library module op for two or more values.

The next operators are available:

If source and target are both of type array and the operator is one of the next ones, the arrays are converted to set before applying the operator:

Example

The next example checks that the configuration field tool.isort.sections is a superset or equal to the array of strings ['STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'] appyling the operator <=.

These comparations are easier to do than checking every item in the array with the built-in JMESPath function contains().

{
  rules: [
    {
      files: ["pyproject.toml"],
      JMESPathsMatch: [
        ["type(tool.isort)", "object"],
        ["type(tool.isort.sections)", "array"],
        [
          "op(['STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'], '<=', tool.isort.sections)",
          true,
        ],
      ],
    },
  ],
}

You can pass multiple operators and values after the target argument chaining the operation with multiple operators. For example:

op(`5`, '+', `3`, '-', `4`)

New in version 0.1.0.

Changed in version 0.4.0: Convert to set before applying operators if both arguments are arrays.

Changed in version 0.7.4: Multiple optional operator and values can be passed as positional arguments.

shlex_split(cmd_str: str) list

Split a string using the Python’s built-in function shlex.split().

New in version 0.4.0.

shlex_join(cmd_list: list[str]) str

Join a list of strings using the Python’s built-in function shlex.join().

New in version 0.4.0.

round(number: float[, precision: int]) float

Round a number to a given precision using the Python’s built-in function round().

New in version 0.5.0.

range([start: float, ]stop: float[, step: float]) list

Return an array of numbers from start to stop with a step of step casting the result of the constructor range to an array.

New in version 0.5.0.

count(value: str | list, sub: any[, start: int[, end: int]]) int

Return the number of occurrences of sub in value using str.count(). If start and end are given, return the number of occurrences between start and end.

New in version 0.5.0.

find(string: str | list, sub: any[, start: int[, end: int]]) int

Return the lowest index in value where subvalue sub is found. If start and end are given, return the number of occurrences between start and end. If not found, -1 is returned.

If value is a string it uses internally the Python’s built-in function str.find(). If value is an array, uses the method str.index().

New in version 0.5.0.

isalnum(string: str) bool

Return True if all characters in string are alphanumeric using str.isalnum().

New in version 0.5.0.

isalpha(string: str) bool

Return True if all characters in string are alphabetic using str.isalpha().

New in version 0.5.0.

isascii(string: str) bool

Return True if all characters in string are ASCII using str.isascii().

New in version 0.5.0.

isdecimal(string: str) bool

Return True if all characters in string are decimal using str.isdecimal().

New in version 0.5.0.

isdigit(string: str) bool

Return True if all characters in string are digits using str.isdigit().

New in version 0.5.0.

isidentifier(string: str) bool

Return True if all characters in string are identifiers if the string is a valid identifier according to the Python language definition using str.isidentifier().

New in version 0.5.0.

islower(string: str) bool

Return True if all characters in string are lowercase using str.islower().

New in version 0.5.0.

isnumeric(string: str) bool

Return True if all characters in string are numeric using str.isnumeric().

New in version 0.5.0.

isprintable(string: str) bool

Return True if all characters in string are printable using str.isprintable().

New in version 0.5.0.

isspace(string: str) bool

Return True if all characters in string are whitespace using str.isspace().

New in version 0.5.0.

istitle(string: str) bool

Return True if all characters in string are titlecased using str.istitle().

New in version 0.5.0.

isupper(string: str) bool

Return True if all characters in string are uppercase using str.isupper().

New in version 0.5.0.

lower(string: str) str

Return a lowercased version of the string using str.lower().

New in version 0.5.0.

lstrip(string: str[, chars: str]) str

Return a left-stripped version of the string using str.lstrip().

New in version 0.5.0.

partition(string: str, sep: str) list[str]

Return an array of 3 items containing the part before the separator, the separator itself, and the part after the separator.

New in version 0.5.0.

rfind(string: str | list, sub: any[, start: int[, end: int]]) int

Return the highest index in value where subvalue sub is found. If start and end are given, return the number of occurrences between start and end. If not found, -1 is returned. If value is a string it uses internally the Python’s built-in function str.find() or str.index() if value is an array.

New in version 0.5.0.

rpartition(string: str, sep: str) list[str]

Return an array of 3 items containing the part after the separator, the separator itself, and the part before the separator splitting the string at the last occurrence of sep.

New in version 0.5.0.

rsplit(string: str[, sep: str[, maxsplit: int]]) list[str]

Return a list of the words in the string, using sep as the delimiter string as returned from the method str.rsplit(). Except for splitting from the right, rsplit() behaves like split().

New in version 0.5.0.

split(string: str[, sep: str[, maxsplit: int]]) list[str]

Return a list of the words in the string, using sep as the delimiter string as returned from the method str.split(). If sep is not given, it defaults to None, meaning that any whitespace string is a separator.

New in version 0.5.0.

splitlines(string: str[, keepends: bool]) list[str]

Return a list of the lines in the string, breaking at line boundaries using the method str.splitlines().

New in version 0.5.0.

swapcase(string: str) str

Return a swapped-case version of the string using str.swapcase().

New in version 0.5.0.

title(string: str) str

Return a titlecased version of the string using str.title().

New in version 0.5.0.

upper(string: str) str

Return an uppercased version of the string using str.upper().

New in version 0.5.0.

zfill(string: str, width: int) str

Return a zero-padded version of the string using str.zfill().

New in version 0.5.0.

enumerate(string: str | list | dict) list[list[int, str]]

Return an array of arrays containing the index and value of each item in the iterable. If the iterable is an object, the value is converted before using to_items().

New in version 0.5.0.

to_items(string: dict) list[list[str, any]]

Convert an object to an array of arrays containing the key and value of each item.

New in version 0.5.0.

from_items(items: list[list[str, any]]) dict

Convert an array of arrays containing the key and value of each item to an object.

New in version 0.5.0.

File system functions

The next functions are useful for file system operations.

isfile(path: str) bool

Return true if the given path is a file, false otherwise.

New in version 0.8.0.

isdir(path: str) bool

Return true if the given path is a directory, false otherwise.

New in version 0.8.0.

exists(path: str) bool

Return true if the given path exists, false otherwise.

New in version 0.8.0.

listdir(path: str) list[str] | null

Return a list of the files and directories in the given path. If the path does not exist, return null.

New in version 0.8.0.

mkdir(path: str) bool

Create a directory in the given path. Return true if the directory has been created, false if it already exists.

New in version 0.8.0.

rmdir(path: str) bool

Remove the directory in the given path. Return true if the directory has been removed, false if it does not exist.

New in version 0.8.0.

glob(pattern: str[, recursive: bool = false]) list[str]

Return a list of the files and directories in the given path using the glob module. If recursive is true, the pattern ** will match any files and zero or more directories, subdirectories and symbolic links to directories.

New in version 0.8.0.

Updater functions

The next functions take an value as the first argument, make some update on this base object and return it updated. Useful for fix queries when you need to return fixed contents for files.

update(base: dict, next: dict) dict

Update the base object with the next object using Python’s builtin dict.update().

Returns the updated base object.

New in version 0.7.0.

insert(base: list, index: int, item: t.Any) list

Insert a item at the given index in the array base.

Returns the updated base array.

New in version 0.7.0.

deepmerge(base: dict, next: dict, strategy: str | list = 'conservative_merger') dict

Merge the base and next objects using the library deepmerge.

Returns the updated base object.

The argument of strategy controls how the objects are merged. It can accept strings with deepmerge strategy names:

Example

deepmerge(@, `{"foo": "bar"}`, 'always_merger')

Or an array with 3 values, the same that takes the class deepmerge.merger.Merger as arguments:

  • type_strategies

  • fallback_strategies

  • type_conflict_strategies

Example

deepmerge(
   @,
   `{"foo": ["bar"]}`,
   `[[["list", "append"], ["dict": "merge"]], ["override"], ["override"]]`
)

New in version 0.7.0.

set(base: dict, key: str, value: t.Any) dict

Set the value of the key in the base object to value.

Returns the updated base object.

New in version 0.7.0.

unset(base: dict, key: str) dict

If has it, remove the key from the base object.

Returns the updated base object.

New in version 0.7.0.

replace(base: str, old: str, new: str[, count: int | None = None])

Replace the old string with the new string in the base string using the Python’s built-in string method str.replace().

Returns the updated base string.

New in version 0.7.0.

removeprefix(string: str, prefix: str) str

Return a string with the given prefix removed using str.removeprefix().

New in version 0.5.0.

removesuffix(string: str, suffix: str) str

Return a string with the given suffix removed using str.removesuffix().

New in version 0.5.0.

format(schema: str, *args: any) str

Return a string formatted using the Python’s built-in format() function. The variable schema only accepts numeric indexes delimited by braces {} for positional arguments in *args.

New in version 0.5.0.

strip(string: str[, chars: str]) str

Return a stripped version of the string using str.strip().

New in version 0.5.0.

rstrip(string: str[, chars: str]) str

Return a right-stripped version of the string using str.rstrip().

New in version 0.5.0.

capitalize(string: str) str

Capitalize the first letter of a string using str.capitalize().

New in version 0.5.0.

casefold(string: str) str

Return a casefolded copy of a string using str.casefold().

New in version 0.5.0.

center(string: str, width: int[, fillchar: str]) str

Return centered in a string of length width using str.center().

New in version 0.5.0.

ljust(string: str, width: int[, fillchar: str]) str

Return a left-justified version of the string using str.ljust().

New in version 0.5.0.

rjust(string: str, width: int[, fillchar: str]) str

Return a right-justified version of the string using str.rjust().

New in version 0.5.0.

Github functions

The functions which name starts with gh_ are functions that connect to only Github sources.

gh_tags(repo_owner: str, repo_name: str[, only_semver: bool = False]) list[str]

Return the list of tags of the Github repository, ordered from latest to oldest.

If you pass the third parameter as a true value, only the tags that are following semantic versioning (even if they are prepended with some text like v) will be returned.

This function is really useful setting the rev properties in .pre-commit-config.yaml files.

New in version 0.7.1.

Fix queries

The verbs of the jmespath plugin can fix files by applying a JMESPath query over the previous content of the files. The fix-queries arguments are always optional.

If no fix-query is provided, project-config will attempt to build an expected node tree instance to update the content parsing the other queries arguments and the expected value.

The query will be a syntax like (example merging objects):

deepmerge(@, `{ "foo": "bar" }`)

Where @ is the previous content of the file.

The result from this JMESPath expression will be the next content of the file. So these transformer functions like deepmerge(), insert() or update() allow you to edit your files with total flexibility.

Automatic fixes

Queries that are simple can be automatically fixed by the plugin. For example, a constant query with their expected value:

{
   "name": "my-project"
}

Currently, is possible to automatically fix the following cases:

  • Query to constant.

  • Query to constant in nested objects.

  • Expression using the type function like type(foo.bar) with expected value as 'array' (creates {foo: {bar: []}} nodes if doesn’t exists before).

  • Indexed expressions with indexes like type(foo[0].bar) with expected value as 'object' (prepends, {bar: {}} to the array bar, creating it if does not exists).

  • Forbidden key in root object like contains(keys(@), 'foo') with expected value as false (removes the key foo from the root object).

JMESPathsMatch

Compares a set of JMESPath expressions against results.

Object-serializes each file in the files property of the rule and executes each expression given in the first item of the tuples passed as value. If a result don’t match, report an error.

Example

The .editorconfig file must have the next content:

root = true

[*]
end_of_line = lf
charset = utf-8
indent_style = space
trim_trailing_whitespace = true
{
  rules: [
    {
      files: [".editorconfig"],
      JMESPathsMatch: [
        ['"".root', true],
        ['"*".end_of_line', "lf"],
        ['"*".indent_style', "space"],
        ['"*".charset', "utf-8"],
        ['"*".trim_trailing_whitespace', true],
      ],
    }
  ]
}

New in version 0.1.0.

Changed in version 0.7.0: The verb also accepts fix queries as third item of rows.

crossJMESPathsMatch

JMESPaths matching between multiple files.

Accept an array of arrays. Each one of these arrays must have the syntax:

[
  "filesJMESPathExpression",  // expression to query each file in `files` property of the rule
  ["otherFile.ext", "JMESPathExpression"]...,  // optionally other files
  "finalJMESPathExpression",  // an array with results of previous expressions as input
  expectedValue,  // value to compare with the result of final JMESPath expression
]

The executed steps are:

  1. For each object-serialized file in files property of the rule.

  2. Execute "filesJMESPathExpression" and append the result to a temporal array.

  3. For each pair of ["otherFile.ext", "JMESPathExpression"], execute "JMESPathExpression" against the object-serialized version of "otherFile.ext" and append each result to the temporal array.

  4. Execute "finalJMESPathExpression" against the temporal array.

  5. Compare the final result with expectedValue and raise error if not match.

Tip

Other file paths can be URLs if you want to match against online sources.

Example

The release field of a Sphinx configuration defined in a file docs/conf.py must be the same that the version of the project metadata defined in th file pyproject.toml, field project.version:

{
  rules: [
    {
      files: ["pyproject.toml"],
      crossJMESPathsMatch: [
        [
          "project.version",
          ["docs/conf.py", "release"],
          "op([0], '==', [1])",
          true,
        ],
      ],
      hint: "Versions of documentation and metadata must be the same"
    }
  ]
}

Note that you can pass whatever number of other files, even 0 and just apply files and final expressions to each file in files property of the rule. For example, the next configuration would not raise errors:

{
  rules: [
    {
      files: ["foo.json"],
      crossJMESPathsMatch: [
        ["bar", "[0].baz", 7],
      ]
    }
  ]
}

You can also override the Objects serialization to use for opening other files using file/path.ext?serializer syntax. For example, to open a Python file line by line:

foo = True
bar = False

New in version 0.4.0.

ifJMESPathsMatch

Compares a set of JMESPath expressions against results.

JSON-serializes each file in the ifJMESPathsMatch property of the rule and executes each expression given in the first item of the tuples passed as value for each file. If a result don’t match, skips the rule.

Example

If inline-quotes config of flake8 is defined to use double quotes, black must be configured as the formatting tool in pyproject.toml:

{
  rules: [
    {
      files: ["pyproject.toml"],
      ifJMESPathsMatch: {
        "pyproject.toml": [
           ["tool.flakeheaven.inline_quotes", "double"],
         ],
      },
      JMESPathsMatch: [
        ["contains(keys(@), 'tool')", true],
        ["contains(keys(tool), 'black')", true],
      }
    }
  ]
}

New in version 0.1.0.

pre-commit

Plugins related to pre-commit hooks and configuration.

preCommitHookExists

Check if multiple pre-commit hooks exists in .pre-commit-config.yaml configuration, optionally defining the settings for each one.

It accepts a tuple with two elements:

  • A string with the URL of the repository for the repo key.

  • A string with th id of a hook or an array with the configurations for each hook. This array also accepts strings with the hook ids.

Examples

{
  rules: [
    {
      files: [".pre-commit-config.yaml"],
      preCommitHookExists: [
        "https://github.com/mondeja/project-config",
        "project-config",
      ]
    }
  ]
}
{
  rules: [
    {
      files: [".pre-commit-config.yaml"],
      preCommitHookExists: [
        "https://github.com/mondeja/project-config",
        "[{"id": "project-config"}]",
      ]
    }
  ]
}

The rev key is added automatically to the configuration of the repositories if not exists in fix mode.

New in version 0.9.1.